├── project ├── build.properties ├── scripted.sbt └── plugins.sbt ├── src ├── sbt-test │ └── shading │ │ ├── files │ │ ├── templates │ │ │ ├── Unpackaged.scala │ │ │ └── foo │ │ │ │ ├── Unshaded.scala │ │ │ │ └── v0 │ │ │ │ └── Shaded.scala │ │ ├── build.sbt │ │ ├── project │ │ │ └── plugins.sbt │ │ └── test │ │ └── artifact-name │ │ ├── test │ │ ├── project │ │ └── plugins.sbt │ │ └── build.sbt ├── main │ └── scala │ │ └── com │ │ └── rallyhealth │ │ └── sbt │ │ └── shading │ │ ├── ProjectNameUnshadedException.scala │ │ ├── ShadingImplicits.scala │ │ ├── ShadeableModuleID.scala │ │ ├── fileShadingErrors.scala │ │ ├── ShadingPlugin.scala │ │ └── Shading.scala └── test │ └── scala │ └── com │ └── rallyhealth │ └── sbt │ └── shading │ ├── ShadeableModuleIDSpec.scala │ └── ShadingTest.scala ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.md │ └── FEATURE_REQUEST.md └── PULL_REQUEST_TEMPLATE.md ├── readme ├── screenshot.png ├── dependency-hell-v1.png ├── dependency-hell-v2.png └── dependency-hell-shaded.png ├── .pre-commit-config.yaml ├── scripted.sbt ├── LICENSE ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── INDIVIDUAL_CONTRIBUTOR_LICENSE.md └── README.md /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.2.3 2 | -------------------------------------------------------------------------------- /src/sbt-test/shading/files/templates/Unpackaged.scala: -------------------------------------------------------------------------------- 1 | class Unpackaged 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # https://help.github.com/articles/about-codeowners/ 2 | * @htmldoug 3 | -------------------------------------------------------------------------------- /src/sbt-test/shading/files/templates/foo/Unshaded.scala: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | class Unshaded 4 | -------------------------------------------------------------------------------- /src/sbt-test/shading/files/templates/foo/v0/Shaded.scala: -------------------------------------------------------------------------------- 1 | package foo.v0 2 | 3 | class Shaded 4 | -------------------------------------------------------------------------------- /project/scripted.sbt: -------------------------------------------------------------------------------- 1 | libraryDependencies += "org.scala-sbt" %% "scripted-plugin" % sbtVersion.value 2 | -------------------------------------------------------------------------------- /readme/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rallyhealth/sbt-shading/HEAD/readme/screenshot.png -------------------------------------------------------------------------------- /readme/dependency-hell-v1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rallyhealth/sbt-shading/HEAD/readme/dependency-hell-v1.png -------------------------------------------------------------------------------- /readme/dependency-hell-v2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rallyhealth/sbt-shading/HEAD/readme/dependency-hell-v2.png -------------------------------------------------------------------------------- /readme/dependency-hell-shaded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rallyhealth/sbt-shading/HEAD/readme/dependency-hell-shaded.png -------------------------------------------------------------------------------- /src/sbt-test/shading/artifact-name/test: -------------------------------------------------------------------------------- 1 | > assertName artifact-name-v0 2 | 3 | > 'set version := "1.0.0"' 4 | > assertName artifact-name-v1 5 | 6 | > 'set version := "2.0.0"' 7 | > assertName artifact-name-v2 8 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers += Resolver.bintrayIvyRepo("rallyhealth", "sbt-plugins") 2 | 3 | addSbtPlugin("org.foundweekends" % "sbt-bintray" % "0.5.4") 4 | addSbtPlugin("com.rallyhealth.sbt" %% "sbt-git-versioning" % "1.2.1") 5 | -------------------------------------------------------------------------------- /src/sbt-test/shading/files/build.sbt: -------------------------------------------------------------------------------- 1 | scalaVersion := "2.11.8" 2 | 3 | organization := "com.rallyhealth.test.scripted" 4 | 5 | credentials += Credentials(Path.userHome / ".ivy2" / ".credentials") 6 | 7 | version := "0.0.1" 8 | 9 | enablePlugins(ShadingPlugin) 10 | -------------------------------------------------------------------------------- /src/main/scala/com/rallyhealth/sbt/shading/ProjectNameUnshadedException.scala: -------------------------------------------------------------------------------- 1 | package com.rallyhealth.sbt.shading 2 | 3 | class ProjectNameUnshadedException(shadedVersion: String, projectName: String) extends Exception(s"Project $projectName is not shaded with $shadedVersion") 4 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | - repo: git://github.com/pre-commit/pre-commit-hooks 2 | sha: v0.9.1 3 | hooks: 4 | - id: trailing-whitespace 5 | files: \.(scala|html|erb|slim|haml|ejs|jade|js|coffee|json|rb|md|py|css|scss|less|sh|tmpl|txt|yaml|yml|pp)$ 6 | - id: end-of-file-fixer 7 | - id: check-yaml 8 | - id: check-json 9 | -------------------------------------------------------------------------------- /src/main/scala/com/rallyhealth/sbt/shading/ShadingImplicits.scala: -------------------------------------------------------------------------------- 1 | package com.rallyhealth.sbt.shading 2 | 3 | import sbt.ModuleID 4 | 5 | object ShadingImplicits extends ShadingImplicits 6 | 7 | trait ShadingImplicits { 8 | import scala.language.implicitConversions 9 | 10 | implicit def toShadeableModuleID(value: ModuleID) = new ShadeableModuleID(value) 11 | } 12 | -------------------------------------------------------------------------------- /src/sbt-test/shading/files/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | { 2 | val pluginVersion = System.getProperty("plugin.version") 3 | if (pluginVersion == null) 4 | throw new RuntimeException( 5 | """|The system property 'plugin.version' is not defined. 6 | |Specify this property using the scriptedLaunchOpts -D.""".stripMargin 7 | ) 8 | else addSbtPlugin("com.rallyhealth.sbt" % "sbt-shading" % pluginVersion) 9 | } 10 | -------------------------------------------------------------------------------- /src/sbt-test/shading/artifact-name/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | { 2 | val pluginVersion = System.getProperty("plugin.version") 3 | if (pluginVersion == null) 4 | throw new RuntimeException( 5 | """|The system property 'plugin.version' is not defined. 6 | |Specify this property using the scriptedLaunchOpts -D.""".stripMargin 7 | ) 8 | else addSbtPlugin("com.rallyhealth.sbt" % "sbt-shading" % pluginVersion) 9 | } 10 | -------------------------------------------------------------------------------- /src/sbt-test/shading/artifact-name/build.sbt: -------------------------------------------------------------------------------- 1 | import sbt.complete.DefaultParsers._ 2 | 3 | scalaVersion := "2.11.8" 4 | 5 | organization := "com.rallyhealth.test.scripted" 6 | 7 | credentials += Credentials(Path.userHome / ".ivy2" / ".credentials") 8 | 9 | version := "0.0.1" 10 | 11 | enablePlugins(ShadingPlugin) 12 | 13 | lazy val assertName = inputKey[Unit]("Asserts that the name is a specific value.") 14 | 15 | assertName := { 16 | val actual = name.value 17 | val expected = spaceDelimited("").parsed.head 18 | 19 | assert(expected == actual, s"expected: $expected actual: $actual") 20 | } 21 | -------------------------------------------------------------------------------- /src/test/scala/com/rallyhealth/sbt/shading/ShadeableModuleIDSpec.scala: -------------------------------------------------------------------------------- 1 | package com.rallyhealth.sbt.shading 2 | 3 | import org.scalactic.TypeCheckedTripleEquals 4 | import org.scalatest.FlatSpec 5 | import sbt._ 6 | 7 | class ShadeableModuleIDSpec extends FlatSpec with TypeCheckedTripleEquals { 8 | 9 | "A ModuleID" should "be shaded" in { 10 | val moduleID = "org" % "name" % "1.0.0" 11 | val shadedModuleID = new ShadeableModuleID(moduleID).shaded() 12 | 13 | assert(shadedModuleID.organization == "org") 14 | assert(shadedModuleID.name == "name-v1") 15 | assert(shadedModuleID.revision == "1.0.0") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /scripted.sbt: -------------------------------------------------------------------------------- 1 | // https://www.scala-sbt.org/1.0/docs/Testing-sbt-plugins.html 2 | /** 3 | * The scripted sbt projects also need to know any sbt opt overrides. For example: 4 | * - if the .ivy2 location is in another place 5 | * - if logging options should be changed 6 | */ 7 | lazy val defaultSbtOpts = settingKey[Option[String]]("The contents of the default_sbt_opts env var.") 8 | 9 | defaultSbtOpts := { 10 | sys.env 11 | .collectFirst { case (key, value) if key.equalsIgnoreCase("default_sbt_opts") => value } 12 | } 13 | 14 | scriptedLaunchOpts := { 15 | scriptedLaunchOpts.value ++ 16 | Seq("-Xmx1024M", "-Dplugin.version=" + version.value) ++ 17 | defaultSbtOpts.value 18 | } 19 | -------------------------------------------------------------------------------- /src/sbt-test/shading/files/test: -------------------------------------------------------------------------------- 1 | > shadingCheck 2 | 3 | # A class with no package is not shaded. 4 | $ copy-file templates/Unpackaged.scala src/main/scala/Unpackaged.scala 5 | -> shadingCheck 6 | $ delete src/main/scala/Unpackaged.scala 7 | 8 | # A class with an unshaded package is not shaded. 9 | $ copy-file templates/foo/Unshaded.scala src/main/scala/foo/Unshaded.scala 10 | -> shadingCheck 11 | $ delete src/main/scala/foo/Unshaded.scala 12 | 13 | # A class with a shaded package should pass. 14 | $ copy-file templates/foo/v0/Shaded.scala src/main/scala/foo/v0/Shaded.scala 15 | > shadingCheck 16 | # But not against the wrong version 17 | > 'set version := "1.0.0"' 18 | -> shadingCheck 19 | $ delete src/main/scala/foo/v0/Shaded.scala 20 | -------------------------------------------------------------------------------- /src/main/scala/com/rallyhealth/sbt/shading/ShadeableModuleID.scala: -------------------------------------------------------------------------------- 1 | package com.rallyhealth.sbt.shading 2 | 3 | import sbt.librarymanagement.ModuleID 4 | 5 | /** 6 | * Sugar for defining shaded dependencies. 7 | * 8 | * @example {{{ 9 | * libraryDependencies += 10 | * "com.rallyhealth.core" %% "lib-spartan" % "2.0.0" shaded() 11 | * }}} 12 | */ 13 | class ShadeableModuleID(val moduleID: ModuleID) extends AnyVal { 14 | 15 | /** 16 | * Appends the version string (e.g. "v2") to the artifact name. 17 | */ 18 | def shaded(): ModuleID = { 19 | val versionString = Shading.shadeVersionString(moduleID.revision) 20 | moduleID.withName(s"${moduleID.name}-$versionString") 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### SBT ### 2 | # Simple Build Tool 3 | # http://www.scala-sbt.org/release/docs/Getting-Started/Directories.html#configuring-version-control 4 | 5 | target/ 6 | lib_managed/ 7 | src_managed/ 8 | project/boot/ 9 | .history 10 | .cache 11 | 12 | 13 | ### Intellij ### 14 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 15 | 16 | /*.iml 17 | 18 | ## Directory-based project format: 19 | .idea/ 20 | # if you remove the above rule, at least ignore the following: 21 | 22 | # User-specific stuff: 23 | # .idea/workspace.xml 24 | # .idea/tasks.xml 25 | # .idea/dictionaries 26 | 27 | # Sensitive or high-churn files: 28 | # .idea/dataSources.ids 29 | # .idea/dataSources.xml 30 | # .idea/sqlDataSources.xml 31 | # .idea/dynamic.xml 32 | # .idea/uiDesigner.xml 33 | 34 | # Gradle: 35 | # .idea/gradle.xml 36 | # .idea/libraries 37 | 38 | # Mongo Explorer plugin: 39 | # .idea/mongoSettings.xml 40 | 41 | ## File-based project format: 42 | *.ipr 43 | *.iws 44 | 45 | ## Plugin-specific files: 46 | 47 | # IntelliJ 48 | out/ 49 | 50 | # mpeltonen/sbt-idea plugin 51 | .idea_modules/ 52 | 53 | # JIRA plugin 54 | atlassian-ide-plugin.xml 55 | 56 | # Jenkins 57 | .ivy2 58 | -------------------------------------------------------------------------------- /src/main/scala/com/rallyhealth/sbt/shading/fileShadingErrors.scala: -------------------------------------------------------------------------------- 1 | package com.rallyhealth.sbt.shading 2 | 3 | import sbt._ 4 | 5 | case class FileShadingErrors(file: File, errors: Seq[FileShadingError]) { 6 | 7 | def relativeTo(base: File): FileShadingErrors = copy(file = file.relativeTo(base).getOrElse(file)) 8 | 9 | override def toString: String = s"$file: ${errors.mkString(", ")}" 10 | } 11 | 12 | class FileShadingErrorsException(shadedVersion: String, errors: Seq[FileShadingErrors]) 13 | extends Exception( 14 | errors.mkString(s"""Expected name shaded version "$shadedVersion" for:\n""".stripMargin, "\n", "") 15 | ) 16 | 17 | sealed trait FileShadingError 18 | 19 | /** 20 | * Package declaration does not contain the shaded version. 21 | */ 22 | case class PackageNameUnshaded(wrongName: String) extends FileShadingError 23 | 24 | /** 25 | * Package declaration could not be found in the file. 26 | * 27 | * There is no reason to do this in libraries. 28 | */ 29 | case object PackageNameMissing extends FileShadingError 30 | 31 | /** 32 | * Directory structure does not contain the shaded version. 33 | */ 34 | case object DirectoryUnshaded extends FileShadingError 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to submit a bug 4 | title: "bug: " 5 | labels: "bug" 6 | assignees: "" 7 | --- 8 | 9 | ## Bug Report 10 | 11 | Thanks for reporting an issue, please review the task list below before submitting the 12 | issue. Your issue report will be closed if the issue is incomplete and the below tasks not completed. 13 | 14 | ## Task List 15 | _Put an `x` in the boxes that apply_ 16 | 17 | - [ ] Steps to reproduce provided 18 | - [ ] Stacktrace (if present) provided 19 | - [ ] Example that reproduces the problem uploaded to Github 20 | - [ ] Full description of the issue provided (see below) 21 | 22 | ## Commit or Tag Version 23 | 24 | Specify the commit or tag version the issue occurred in 25 | 26 | ## Expected Behaviour 27 | 28 | Describe the expected behavior 29 | 30 | ## Actual Behaviour 31 | 32 | Tell us what happens instead 33 | 34 | ## Steps to Reproduce 35 | 36 | Please explain the steps required to duplicate the issue, especially if you are able to provide a sample application. 37 | 38 | 1. TODO 39 | 2. TODO 40 | 3. TODO 41 | 42 | ## Related Code 43 | 44 | If you are able to illustrate the bug with an example, please provide it here. 45 | 46 | ``` 47 | insert short code snippets here 48 | ``` 49 | 50 | ## Further comments 51 | 52 | List any other information that is relevant to your issue. Related issues, suggestions on how to fix, Stack Overflow links, forum links, etc. 53 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Proposed changes 2 | 3 | Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. 4 | 5 | ## Types of changes 6 | 7 | What types of changes does your code introduce? 8 | _Put an `x` in the boxes that apply_ 9 | 10 | - [ ] Bugfix (non-breaking change which fixes an issue) 11 | - [ ] New feature (non-breaking change which adds functionality) 12 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 13 | - [ ] Documentation only 14 | 15 | ## Checklist 16 | 17 | _Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ 18 | 19 | - [ ] I have read the [CONTRIBUTING](https://github.com/Optum/oss-template/blob/main/CONTRIBUTING.md) doc 20 | - [ ] I have added tests that prove my fix is effective or that my feature works 21 | - [ ] I have added necessary documentation (if appropriate) 22 | - [ ] Any dependent changes have been merged and published in downstream modules 23 | 24 | ## Further comments 25 | 26 | If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for this project 4 | title: "feature: " 5 | labels: "enhancement" 6 | assignees: "" 7 | --- 8 | 9 | ## Feature Request 10 | 11 | Thanks for showing an interest in continuing to grow this product! Please review the task list below 12 | before submitting the request. Your feature request will be closed if the information is incomplete 13 | and the below tasks are not completed. 14 | 15 | ## Task List 16 | _Put an `x` in the boxes that apply_ 17 | 18 | - [ ] Describe the feature being requested 19 | - [ ] Describe a preferred solution 20 | - [ ] Provide examples or related code if applicable 21 | 22 | **If the feature request is approved, would you be willing to submit a PR?** 23 | _(Help can be provided if you need assistance submitting a PR)_ 24 | - [ ] Yes 25 | - [ ] No 26 | 27 | ## Describe the Feature Request 28 | 29 | Please provide a clear and concise description of what the feature request is. Please include if your 30 | feature request is related to a problem. 31 | 32 | ## Describe Preferred Solution 33 | 34 | Provide a clear and concise description of what you want to happen. 35 | 36 | ## Describe Alternatives 37 | 38 | If applicable, please provide a clear and concise description of any alternative solutions or features 39 | you've considered. 40 | 41 | ## Related Code 42 | 43 | If you are able to illustrate the new feature request with an example, please provide it here. 44 | 45 | ## Further comments 46 | 47 | Any other additional comments on the new feature can be added here. -------------------------------------------------------------------------------- /src/main/scala/com/rallyhealth/sbt/shading/ShadingPlugin.scala: -------------------------------------------------------------------------------- 1 | package com.rallyhealth.sbt.shading 2 | 3 | import sbt.Keys._ 4 | import sbt._ 5 | import sbt.plugins.JvmPlugin 6 | 7 | object ShadingPlugin extends AutoPlugin { 8 | 9 | override def requires: Plugins = { 10 | /** 11 | * We hook test & publish which can only happen after the Defaults are set by the JvmPlugin. 12 | * Otherwise, our hooks will get overridden. 13 | * 14 | * @see http://stackoverflow.com/a/25251194 15 | */ 16 | JvmPlugin 17 | } 18 | 19 | /** 20 | * These are imported into the build.sbt's scope automatically. 21 | */ 22 | object autoImport extends ShadingImplicits { 23 | 24 | lazy val shadingVersionString = settingKey[String]("A version string like 'v2' that all packages and directories must include.") 25 | lazy val shadingCheck = taskKey[Unit]("Runs all checks that the project is fully shaded.") 26 | lazy val shadingCheckFiles = taskKey[Unit]("Checks that all scala directories/packages include the shaded version.") 27 | lazy val shadingCheckArtifactName = taskKey[Unit]("Checks that the project/artifact name includes the shaded version.") 28 | lazy val shadingNameShader = settingKey[String => String]("Adds the shading version string to the name of the artifact.") 29 | } 30 | 31 | import autoImport._ 32 | 33 | /** 34 | * These get applied to any project that calls: .enablePlugins(ShadingPlugin) 35 | */ 36 | override def projectSettings: Seq[Def.Setting[_]] = Seq( 37 | (publish in Compile) := (publish in Compile).dependsOn(shadingCheck).value, 38 | (test in Test) := (test in Test).dependsOn(shadingCheck).value, 39 | shadingCheck := { 40 | shadingCheckArtifactName.value 41 | shadingCheckFiles.value 42 | }, 43 | shadingVersionString := Shading.shadeVersionString(version.value), 44 | shadingCheckArtifactName := { 45 | if (!name.value.contains(shadingVersionString.value)) { 46 | throw new ProjectNameUnshadedException(shadingVersionString.value, name.value) 47 | } 48 | }, 49 | shadingNameShader := { 50 | // Making use of name.value directly results in a circular dependency. 51 | (name: String) => s"${name}-${shadingVersionString.value}" 52 | }, 53 | name := shadingNameShader.value(name.value), 54 | sources in shadingCheckFiles := ((scalaSource in Compile).value ** "*.scala").get, 55 | shadingCheckFiles := { 56 | val scalaFiles = (sources in shadingCheckFiles).value 57 | 58 | val errors = scalaFiles.foldLeft(Seq.newBuilder[FileShadingErrors]) { (builder, file) => 59 | builder ++= Shading.validate(shadingVersionString.value, file) 60 | }.result() 61 | 62 | if (errors.nonEmpty) { 63 | throw new FileShadingErrorsException(shadingVersionString.value, errors.map(_.relativeTo(baseDirectory.value))) 64 | } 65 | } 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project email 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [opensource@optum.com][email]. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | [email]: mailto:opensource@optum.com -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms. Please also review our [Contributor License Agreement ("CLA")](INDIVIDUAL_CONTRIBUTOR_LICENSE.md) prior to submitting changes to the project. You will need to attest to this agreement following the instructions in the [Paperwork for Pull Requests](#paperwork-for-pull-requests) section below. 4 | 5 | --- 6 | 7 | # How to Contribute 8 | 9 | Now that we have the disclaimer out of the way, let's get into how you can be a part of our project. There are many different ways to contribute. 10 | 11 | ## Issues 12 | 13 | We track our work using Issues in GitHub. Feel free to open up your own issue to point out areas for improvement or to suggest your own new experiment. If you are comfortable with signing the waiver linked above and contributing code or documentation, grab your own issue and start working. 14 | 15 | ## Coding Standards 16 | 17 | We have some general guidelines towards contributing to this project. 18 | 19 | ### Languages 20 | 21 | *Scala* 22 | 23 | ## Pull Requests 24 | 25 | If you've gotten as far as reading this section, then thank you for your suggestions. 26 | 27 | ## Paperwork for Pull Requests 28 | 29 | - Please read this guide and make sure you agree with our [Contributor License Agreement ("CLA")](INDIVIDUAL_CONTRIBUTOR_LICENSE.md). 30 | - Make sure git knows your name and email address: 31 | ``` 32 | $ git config user.name "J. Random User" 33 | $ git config user.email "j.random.user@example.com" 34 | ``` 35 | > The name and email address must be valid as we cannot accept anonymous contributions. 36 | - Write good commit messages. 37 | > Concise commit messages that describe your changes help us better understand your contributions. 38 | - The first time you open a pull request in this repository, you will see a comment on your PR with a link that will allow you to sign our Contributor License Agreement (CLA) if necessary. 39 | > The link will take you to a page that allows you to view our CLA. You will need to click the `Sign in with GitHub to agree button` and authorize the cla-assistant application to access the email addresses associated with your GitHub account. Agreeing to the CLA is also considered to be an attestation that you either wrote or have the rights to contribute the code. All committers to the PR branch will be required to sign the CLA, but you will only need to sign once. This CLA applies to all repositories in the Optum org. 40 | 41 | ## General Guidelines 42 | 43 | Ensure your pull request (PR) adheres to the following guidelines: 44 | 45 | - Try to make the name concise and descriptive. 46 | - Give a good description of the change being made. Since this is very subjective, see the [Updating Your Pull Request (PR)](#updating-your-pull-request-pr) section below for further details. 47 | - Every pull request should be associated with one or more issues. If no issue exists yet, please create your own. 48 | - Make sure that all applicable issues are mentioned somewhere in the PR description. This can be done by typing # to bring up a list of issues. 49 | 50 | ### Updating Your Pull Request (PR) 51 | 52 | A lot of times, making a PR adhere to the standards above can be difficult. If the maintainers notice anything that we'd like changed, we'll ask you to edit your PR before we merge it. This applies to both the content documented in the PR and the changed contained within the branch being merged. There's no need to open a new PR. Just edit the existing one. 53 | 54 | [email]: mailto:opensource@optum.com 55 | -------------------------------------------------------------------------------- /src/main/scala/com/rallyhealth/sbt/shading/Shading.scala: -------------------------------------------------------------------------------- 1 | package com.rallyhealth.sbt.shading 2 | 3 | import sbt._ 4 | 5 | import scala.io.Source 6 | 7 | object Shading { 8 | 9 | def validate(shadedVersion: String, f: File): Option[FileShadingErrors] = { 10 | val errors = validateDirName(shadedVersion, f) ++ validatePackageName(shadedVersion, f) 11 | 12 | if (errors.nonEmpty) { 13 | Some(FileShadingErrors(f, errors.toSeq)) 14 | } else { 15 | None 16 | } 17 | } 18 | 19 | /** 20 | * Checks the file's parent directories to make sure there is one that exactly matches the shadedVersion. 21 | * 22 | * For shadedVersion "v2": 23 | * 24 | * Valid: "src/main/scala/com/rallyhealth/whatever/v2/File.scala" 25 | * 26 | * Invalid: "src/main/scala/com/rallyhealth/whatever/File.scala" // because shaded version is missing. 27 | * Invalid: "src/main/scala/com/rallyhealth/v1/whatever/File.scala" // because shaded version should be "v2", not "v1" 28 | */ 29 | def validateDirName(shadedVersion: String, f: File): Option[FileShadingError] = { 30 | if (parentDirs(f).map(_.getName).contains(shadedVersion)) { 31 | None 32 | } else { 33 | Some(DirectoryUnshaded) 34 | } 35 | } 36 | 37 | /** 38 | * Scans the file contents for the package definition and verifies that it includes the shadedVersion. 39 | * 40 | * For shadedVersion "v2": 41 | * 42 | * Valid: "com.rallyhealth.whatever.v2" 43 | * 44 | * Invalid: "com.rallyhealth.whatever" // because shaded version is missing. 45 | * Invalid: "com.rallyhealth.v1.whatever" // because shaded version should be "v2", not "v1" 46 | * 47 | */ 48 | def validatePackageName(shadedVersion: String, f: File): Option[FileShadingError] = { 49 | extractPackage(f) match { 50 | case None => Some(PackageNameMissing) // not okay for shared libs. 51 | case Some(p) if p.split('.').contains(shadedVersion) => None 52 | case Some(p) if f.getName == "package.scala" => 53 | 54 | /** 55 | * Special treatment for package objects. 56 | * 57 | * Valid example of com/rallyhealth/whatever/v2/package.scala: 58 | * 59 | * {{{ 60 | * package com.rallyhealth.whatever // normally invalid. 61 | * 62 | * package object v2 { // redeemed! 63 | * // ... 64 | * } 65 | * }}} 66 | */ 67 | extractPackageObjectNames(f).filterNot(_ contains shadedVersion).toList match { 68 | case Nil => None 69 | case firstBaddie :: _ => Some(PackageNameUnshaded(p + "." + firstBaddie)) 70 | } 71 | case Some(p) => Some(PackageNameUnshaded(p)) 72 | } 73 | } 74 | 75 | /** 76 | * Reads the file until it finds a package declaration and extracts the value. 77 | */ 78 | private def extractPackage(scalaFile: File): Option[String] = { 79 | Source.fromFile(scalaFile) 80 | .getLines() 81 | .collectFirst { 82 | case PackageRegex(p) => p 83 | } 84 | } 85 | 86 | /** 87 | * Extracts the package object name from a package.scala. 88 | * 89 | * For example, extracts "v2" from: 90 | * {{{ 91 | * package com.rallyhealth.whatever 92 | * 93 | * package object v2 94 | * }}} 95 | */ 96 | private def extractPackageObjectNames(packageObject: File) = { 97 | Source.fromFile(packageObject) 98 | .getLines() 99 | .collect { 100 | case PackageObjectRegex(packageName) => packageName 101 | } 102 | } 103 | 104 | /** 105 | * Gets a File pointing to each ancestor from the file's parent to root. 106 | */ 107 | private def parentDirs(f: File): Stream[File] = { 108 | Option(f.getParentFile) match { 109 | case None => Stream.empty 110 | case Some(f) => f #:: parentDirs(f) 111 | } 112 | } 113 | 114 | def shadeVersionString(version: String): String = "v" + version.takeWhile(_ != '.') 115 | 116 | /** 117 | * Naively ignores nested packages and perhaps other things that are possible in scala. 118 | * 119 | * Let's cross that bridge when we come to it. 120 | */ 121 | private val PackageRegex = "\\s*package\\s+([^\\s]+)\\s*".r 122 | private val PackageObjectRegex = "\\s*package\\s+object\\s+([^\\s]+).*".r 123 | } 124 | -------------------------------------------------------------------------------- /src/test/scala/com/rallyhealth/sbt/shading/ShadingTest.scala: -------------------------------------------------------------------------------- 1 | package com.rallyhealth.sbt.shading 2 | 3 | import java.io.File 4 | 5 | import org.scalactic.TypeCheckedTripleEquals 6 | import org.scalatest.{FunSpec, Matchers, OptionValues} 7 | import sbt._ 8 | 9 | class ShadingTest extends FunSpec with Matchers with TypeCheckedTripleEquals with OptionValues { 10 | 11 | locally { 12 | val dir = IO.createTemporaryDirectory 13 | 14 | def createFile(name: String)(contents: String): File = { 15 | val file = dir / name 16 | IO.write(file, contents) 17 | file 18 | } 19 | 20 | describe("packages") { 21 | it("approves shaded packages") { 22 | val result = Shading.validatePackageName( 23 | "v2", createFile("GoodPackaged.scala") { 24 | """ 25 | |package com.rallyhealth.v2 26 | | 27 | |class GoodPackaged 28 | """.stripMargin 29 | } 30 | ) 31 | assert(result === None) 32 | } 33 | 34 | it("fails unshaded packages") { 35 | val result = Shading.validatePackageName( 36 | "v2", createFile("BadUnshadedPackage.scala") { 37 | """ 38 | |package com.rallyhealth.unshaded 39 | | 40 | |class BadUnshadedPackage 41 | """.stripMargin 42 | } 43 | ) 44 | assert(result === Some(PackageNameUnshaded("com.rallyhealth.unshaded"))) 45 | } 46 | 47 | it("fails missing packages") { 48 | val result = Shading.validatePackageName( 49 | "v2", createFile("Unpackaged.scala") { 50 | """ 51 | |class Unpackaged 52 | """.stripMargin 53 | } 54 | ) 55 | assert(result === Some(PackageNameMissing)) 56 | } 57 | } 58 | 59 | describe("package objects") { 60 | it("approves package objects with version in package object") { 61 | val result = Shading.validatePackageName( 62 | "v2", createFile("package.scala") { 63 | """ 64 | |package com.rallyhealth.whatever 65 | | 66 | |package object v2 67 | """.stripMargin 68 | } 69 | ) 70 | assert(result === None) 71 | } 72 | 73 | it("approves package objects with version in parent package") { 74 | val result = Shading.validatePackageName( 75 | "v2", createFile("package.scala") { 76 | """ 77 | |package com.rallyhealth.whatever.v2 78 | | 79 | |package object whatever 80 | """.stripMargin 81 | } 82 | ) 83 | assert(result === None) 84 | } 85 | 86 | it("fails unshaded package objects") { 87 | val result = Shading.validatePackageName( 88 | "v2", createFile("package.scala") { 89 | """ 90 | |package com.rallyhealth.whatever 91 | | 92 | |package object notVersioned 93 | """.stripMargin 94 | } 95 | ) 96 | assert(result === Some(PackageNameUnshaded("com.rallyhealth.whatever.notVersioned"))) 97 | } 98 | } 99 | 100 | describe("directories") { 101 | it("approves shaded directories") { 102 | val file = dir / "com" / "rallyhealth" / "v2" / "GoodShaded.scala" 103 | file.mkdirs() 104 | file.createNewFile() 105 | 106 | val result = Shading.validateDirName("v2", file) 107 | assert(result === None) 108 | } 109 | 110 | it("fails unversioned directories") { 111 | val file = dir / "BadUnshaded.scala" 112 | file.createNewFile() 113 | val result = Shading.validateDirName("v2", file) 114 | assert(result === Some(DirectoryUnshaded)) 115 | } 116 | } 117 | 118 | describe("validate") { 119 | it("should validate all the things") { 120 | val file = dir / "com" / "rallyhealth" / "bad" / "Bad.scala" 121 | IO.write( 122 | file, 123 | """ 124 | |package com.rallyhealth.bad 125 | | 126 | |trait Unshaded 127 | """.stripMargin 128 | ) 129 | val result = Shading.validate("v2", file) 130 | result.value.errors should contain allOf (DirectoryUnshaded, PackageNameUnshaded("com.rallyhealth.bad")) 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /INDIVIDUAL_CONTRIBUTOR_LICENSE.md: -------------------------------------------------------------------------------- 1 | # Individual Contributor License Agreement ("Agreement") V2.0 2 | 3 | Thank you for your interest in this Optum project (the "PROJECT"). In order to clarify the intellectual property license granted with Contributions from any person or entity, the PROJECT must have a Contributor License Agreement ("CLA") on file that has been signed by each Contributor, indicating agreement to the license terms below. This license is for your protection as a Contributor as well as the protection of the PROJECT and its users; it does not change your rights to use your own Contributions for any other purpose. 4 | 5 | You accept and agree to the following terms and conditions for Your present and future Contributions submitted to the PROJECT. In return, the PROJECT shall not use Your Contributions in a way that is inconsistent with stated project goals in effect at the time of the Contribution. Except for the license granted herein to the PROJECT and recipients of software distributed by the PROJECT, You reserve all right, title, and interest in and to Your Contributions. 6 | 1. Definitions. 7 | 8 | "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with the PROJECT. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 9 | 10 | "Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to the PROJECT for inclusion in, or documentation of, any of the products owned or managed by the PROJECT (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the PROJECT or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the PROJECT for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." 11 | 12 | 2. Grant of Copyright License. 13 | 14 | Subject to the terms and conditions of this Agreement, You hereby grant to the PROJECT and to recipients of software distributed by the PROJECT a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works. 15 | 16 | 3. Grant of Patent License. 17 | 18 | Subject to the terms and conditions of this Agreement, You hereby grant to the PROJECT and to recipients of software distributed by the PROJECT a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed. 19 | 20 | 4. Representations. 21 | 22 | (a) You represent that you are legally entitled to grant the above license. If your employer(s) has rights to intellectual property that you create that includes your Contributions, you represent that you have received permission to make Contributions on behalf of that employer, that your employer has waived such rights for your Contributions to the PROJECT, or that your employer has executed a separate Corporate CLA with the PROJECT. 23 | 24 | (b) You represent that each of Your Contributions is Your original creation (see section 6 for submissions on behalf of others). You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of Your Contributions. 25 | 26 | 5. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. 27 | 28 | 6. Should You wish to submit work that is not Your original creation, You may submit it to the PROJECT separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]". 29 | 30 | 7. You agree to notify the PROJECT of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sbt-shading 2 | 3 | Enforces that a library's components are all versioned. 4 | 5 | #### For example: 6 | 7 | | What | Incorrect | Correct | 8 | | ------------- | --------------------------- | ---------------------------------- | 9 | | Artifact name | lib-stats | lib-stats-**v2** | 10 | | Packages | com.rallyhealth.stats.Stats | com.rallyhealth.stats.**v2**.Stats | 11 | | Directories | com/rallyhealth/stats/Stats | com/rallyhealth/stats/**v2**/Stats | 12 | 13 | Mitigates runtime dependency hell by enabling multiple incompatible major versions of a library to coexist together safely. 14 | 15 | ## Setup 16 | 17 | Add the latest [release](https://github.com/AudaxHealthInc/sbt-shading/releases) to your plugins.sbt: 18 | 19 | plugins.sbt 20 | 21 | ``` 22 | addSbtPlugin("com.rallyhealth.sbt" %% "sbt-shading" % "x.y.z") 23 | ``` 24 | 25 | build.sbt 26 | 27 | ``` 28 | lazy val `lib-stats` = project 29 | .enablePlugins(ShadingPlugin) 30 | ``` 31 | 32 | Add `v2` to your packages: 33 | 34 | | Incorrect | Correct | 35 | | ------------------------------------------ | ------------------------------------------------ | 36 | | src/**main**/scala/com/rallyhealth/foo/... | src/**main**/scala/com/rallyhealth/foo/**v2**... | 37 | | src/**test**/scala/com/rallyhealth/foo/... | src/**test**/scala/com/rallyhealth/foo/**v2**... | 38 | | src/**it**/scala/com/rallyhealth/foo/... | src/**it**/scala/com/rallyhealth/foo/**v2**... | 39 | 40 | ## Usage 41 | 42 | Shading is checked automatically during the following SBT tasks 43 | 44 | - publish 45 | - test 46 | 47 | Shading is not checked during `compile` because it does not affect compilation: your code 48 | should compile fine whether it is shaded or not. Shading is a correctness test (like unit 49 | tests). 50 | 51 | If you want to check shading yourself you can run 52 | 53 | ``` 54 | sbt> rallyShadingCheck 55 | ``` 56 | 57 | or 58 | 59 | ``` 60 | $ sbt rallyShadingCheck 61 | ``` 62 | 63 | When the plugin detects a violation it will print something like this: 64 | 65 | ``` 66 | sbt> rallyShadingCheck 67 | [trace] Stack trace suppressed: run last /*:shadingCheckFiles for the full output. 68 | [error] (/*:shadingCheckFiles) com.rallyhealth.sbt.shading.FileShadingErrorsException: Expected name shaded version "v2" for: 69 | [error] src/main/scala/com/rallyhealth/foo/Foo.scala: DirectoryUnshaded, PackageNameUnshaded(com.rallyhealth.foo) 70 | [error] src/main/scala/com/rallyhealth/foo/Bar.scala: DirectoryUnshaded, PackageNameUnshaded(com.rallyhealth.foo) 71 | [error] Total time: 1 s, completed Jul 26, 2018 10:04:47 AM 72 | ``` 73 | 74 | ## Depending on shaded libraries 75 | 76 | If you are _depending_ on a shaded library you simply need to add the "v2" to the _artifact name_ of your `ModuleID` declaration: 77 | 78 | ``` 79 | "com.rallyhealth.foo" %% "lib-stats-v2" % "2.0.0" 80 | ``` 81 | 82 | To avoid repeating the version in two places -- the artifact and version -- you can use a handy implicit: 83 | 84 | ``` 85 | import com.rallyhealth.sbt.shading.ShadingImplicits._ 86 | 87 | "com.rallyhealth.foo" %% "lib-stats" % "2.0.0" shaded 88 | ``` 89 | 90 | ## Why Semver is not enough 91 | 92 | [SemVer](http://semver.org/) alone will not save you from [Dependency Hell](https://en.wikipedia.org/wiki/Dependency_hell). 93 | 94 | ### Major versions bring breaking changes. 95 | 96 | > 8. Major version X (X.y.z | X > 0) MUST be incremented if any backwards incompatible changes are introduced to the public API. 97 | > 98 | > -- http://semver.org/#spec-item-8 99 | 100 | ## A Song of Dependencies and Hell 101 | 102 | ### Chapter 1: Everything works 103 | 104 | Consider a library, `lib-stats % 1.0.0`, that records metrics. It has a `com.rallyhealth.stats.Stats.inc()` method that everybody loves. Your app uses `lib-http`, `lib-queue`, and `lib-akka` which make use of `lib-stats % 1.0.0` to send metrics, and life is good. 105 | 106 | ![Dependency graph. Transitive dependency on lib-stats v1.0.0](readme/dependency-hell-v1.png) 107 | 108 | ### Chapter 2: The breaking change 109 | 110 | 1. One day, a well-meaning developer decides that `Stats.inc()` should really be renamed to `Stats.increment()`. 111 | 1. He renames the method and releases `lib-stats 2.0.0` as a major version, signaling the breaking change. He also updates `lib-queue` to make use of it and releases `2.0.0` of that as well. 112 | 1. You need the latest queue-y goodness for a new feature due tomorrow, so you update to the latest `lib-queue`. Your code compiles. Your unit tests pass. You deploy it to dev. 113 | 1. You're shocked to discover that both HTTP and Caching are broken. 114 | 115 | ![Dependency graph. lib-stats v1.0.0 is evicted by v2.0.0. lib-http and lib-cache are broken.](readme/dependency-hell-v2.png) 116 | 117 | You post the error to the Engineering room. They say "hurr hurr derpendencies," and tell you you'll have to clean up the mess. You need to ship this feature tomorrow. You cry. 118 | 119 | #### Evictions 120 | 121 | So what happened? 122 | 123 | 1. Sbt evicted `lib-stats 1.0.0` in favor of the newer `lib-stats 2.0.0`. That's by design. Sbt will only keep the latest version of a library. 124 | 1. `lib-http` tried to call `Stats.inc()` but there was no such method defined. There was only `Stats.increment()`. 125 | 1. You look up who renamed the method, but immediately regret doing so as impure thoughts flood your mind. 126 | 127 | There must be a better way. 128 | 129 | ### Chapter 3: A Solution Emerges 130 | 131 | #### Eliminating Evictions 132 | 133 | Can we prevent the eviction from happening? Yes! If we give the artifacts different names, they won't get evicted. Including the major version in the name, like `lib-stats-v2`, would do the trick quite nicely. This plugin enforces that. 134 | 135 | #### Preventing Namespace Conflicts 136 | 137 | So if both `lib-stats-v1` and `lib-stats-v2` are on the classpath, how do we keep them from having incompatible versions of `com.rallyhealth.stats.Stats`? 138 | 139 | Simple. This plugin enforces that you include `v2` in the package name. You'll end up with `com.rallyhealth.stats.v1.Stats` and `com.rallyhealth.stats.v2.Stats` living in harmony. 140 | 141 | ![Dependency graph. lib-stats v1 and v2 are both in the graph. All libraries have compatible dependencies.](readme/dependency-hell-shaded.png) 142 | 143 | ### Epilogue 144 | 145 | Your app uses only shaded internal libraries now. You have two binary-incompatible versions of `lib-stats-v*` in your app, but you don't care. Everything works just fine. 146 | 147 | One day you'll update `lib-http` and `lib-cache` to use the new `lib-stats-v2`, but not today. Your friends are heading to the bar, and you're free to join then. 148 | 149 | Your phone vibrates and you see the !love from QA and your product owner telling you how wonderful you are. 150 | 151 | You smile as you sip your gin and tonic. Dependency hell is the last thing on your mind. 152 | 153 | ## Errors 154 | 155 | If the artifact name, directories, or package names do not include the current major version, the [shading checks](https://github.com/AudaxHealthInc/sbt-shading/blob/master/src/main/scala/com/rallyhealth/sbt/shading/ShadingPlugin.scala#L22-L28) will fail the build when `test` or `publish` are run. 156 | 157 | ![screenshot of build output of all three types of shading errors](readme/screenshot.png) 158 | 159 | ## Caveats 160 | 161 | ### Renaming 162 | 163 | To upgrade from `lib-stats-v1` to `lib-stats-v2`, all classes that `import com.rallyhealth.stats.v1.Stats` will have to be updated to `import com.rallyhealth.stats.v2.Stats`. 164 | 165 | This may seem like a drawback at first, but it's actually a strength. Introducing breaking changes has become more expensive. They can often be avoided. There's incentive to do so now. 166 | 167 | ### Third party jars 168 | 169 | This plugin cannot protect you from unshaded third party libraries. For example, netty 3 does not play nicely with netty 4. However, third party libs move at much slower pace than Rally's internal libs. Historically, most of our pain has been self-induced via internal libs. 170 | 171 | ### Resource management 172 | 173 | Take care around using shading with global state and resource consumption (threads, external connections, etc.). For example, if your library acts as a global database connection pool, shading it opens the possibility of having multiple instances active at once and opening extra connections. 174 | 175 | If the consequences of duplicating global state is unacceptable, shading may not be the correct solution. 176 | 177 | ### Minor/Patch breakages 178 | 179 | This plugin only solves for breaking changes across major versions. 180 | 181 | [sbt-git-versioning](https://github.com/rallyhealth/sbt-git-versioning) complements this plugin by providing SemVer enforcement for minor/patch releases using the Typesafe [Migration Manager](https://github.com/typesafehub/migration-manager). 182 | 183 | ## Alternatives 184 | 185 | ### OSGi 186 | 187 | A framework for managing components on the JVM. Requires running inside a third party OSGi server. 188 | 189 | - https://www.osgi.org/ 190 | - https://en.wikipedia.org/wiki/OSGi 191 | 192 | ### Java 9: Project Jigsaw 193 | 194 | Released with Java 9. This solves a somewhat different problem: breaking down monolithic JARs -- namely the JDK -- 195 | into smaller, more independent parts. It allows a JAR to more explicitly define what classes/packages are being exposed 196 | and what transitive dependencies are being shared with the consuming application. 197 | 198 | Modules _can_ solve the problem of two different JARs each defining their own _internal_ `com.acme.Foo` and the 199 | `ClassLoader` properly loading the correct class for each JAR. If the JARs properly declare `com.acme.Foo` as 200 | internal then there should be no conflict. 201 | 202 | Unfortunately modules do _not_ solve the problem if both JARs expose their own `com.acme.Foo` definition. The 203 | `ClassLoader` will still have to guess. Modules also do _not_ help if you depend on different versions of the 204 | same JAR since Jigsaw [explicitly does not handle versions](http://openjdk.java.net/projects/jigsaw/goals-reqs/03#versioning). 205 | 206 | TL;DR: Jigsaw is a _partial_ solution 207 | 208 | - [Home](http://openjdk.java.net/projects/jigsaw/) 209 | - [Quick Start](http://openjdk.java.net/projects/jigsaw/quick-start) 210 | - [Java Platform Module System (JSR 376)](http://openjdk.java.net/projects/jigsaw/spec/) 211 | - [Understanding Java 9 Modules](https://www.oracle.com/corporate/features/understanding-java-9-modules.html) 212 | - [Code First Java 9 Module System Tutorial](https://blog.codefx.org/java/java-module-system-tutorial/) 213 | - [What’s ahead with Java 9 & Project Jigsaw](https://www.dynatrace.com/news/blog/whats-ahead-with-java-9-project-jigsaw/) 214 | - [Java 9 Migration Guide: The Seven Most Common Challenges](https://blog.codefx.org/java/java-9-migration-guide/) 215 | - [The Top 10 Jigsaw and Java 9 Misconceptions Debunked](https://blog.takipi.com/the-top-10-jigsaw-and-java-9-misconceptions-debunked/) 216 | - [Is Jigsaw good or is it wack?](https://blog.plan99.net/is-jigsaw-good-or-is-it-wack-ec634d36dd6f) 217 | 218 | ### sbt-assembly shading 219 | 220 | sbt-assembly can do auto-shading of artifacts, but gets hairy with multi-project builds and IDE support. 221 | 222 | - https://github.com/sbt/sbt-assembly#shading 223 | 224 | ## Maintainer 225 | 226 | You can ping [Doug Roper](mailto:doug.roper@rallyhealth.com) for any questions. 227 | 228 | ## Testing 229 | 230 | This plugin is tested by unit tests and with sbt's built-in [scripted plugin](http://eed3si9n.com/testing-sbt-plugins). 231 | --------------------------------------------------------------------------------