Repository: VirtusLab/scala-cli Branch: main Commit: fbc634d33242 Files: 1105 Total size: 4.5 MB Directory structure: gitextract_updn7qcx/ ├── .dockerignore ├── .git-blame-ignore-revs ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── feature_request.md │ │ ├── new_release.md │ │ └── other.md │ ├── actions/ │ │ └── windows-reg-import/ │ │ └── action.yml │ ├── ci/ │ │ └── windows/ │ │ └── custom.reg │ ├── dependabot.yml │ ├── pull_request_template.md │ ├── release/ │ │ ├── release-notes-regexes.md │ │ ├── release-procedure.md │ │ └── windows-antimalware-analysis.md │ ├── scripts/ │ │ ├── build-website.sh │ │ ├── check-cross-version-deps.sc │ │ ├── check-override-keywords.sh │ │ ├── choco/ │ │ │ ├── scala-cli.nuspec │ │ │ └── tools/ │ │ │ └── chocolateyinstall.ps1 │ │ ├── classify-changes.sh │ │ ├── docker/ │ │ │ ├── ScalaCliDockerFile │ │ │ └── ScalaCliSlimDockerFile │ │ ├── generate-docker-image.sh │ │ ├── generate-junit-reports.sc │ │ ├── generate-native-image.sh │ │ ├── generate-os-packages.sh │ │ ├── generate-slim-docker-image.sh │ │ ├── get-latest-cs.sh │ │ ├── gpg-setup.sh │ │ ├── process_release_notes.sc │ │ ├── publish-docker-images.sh │ │ ├── publish-sdkman.sh │ │ ├── publish-slim-docker-images.sh │ │ ├── scala-cli.rb.template │ │ ├── scala.rb.template │ │ ├── update-website.sh │ │ └── verify_old_cpus.sh │ └── workflows/ │ ├── ci.yml │ ├── publish-docker.yml │ ├── test-report.yml │ └── website.yaml ├── .gitignore ├── .mill-jvm-opts ├── .mill-version ├── .scala-steward.conf ├── .scalafix.conf ├── .scalafix3.conf ├── .scalafmt.conf ├── AGENTS.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DEV.md ├── Dockerfile ├── INTERNALS.md ├── LICENSE ├── LLM_POLICY.md ├── README.md ├── agentskills/ │ ├── README.md │ ├── adding-directives/ │ │ └── SKILL.md │ └── integration-tests/ │ └── SKILL.md ├── build.mill ├── gcbenchmark/ │ ├── .gitignore │ ├── README.md │ └── gcbenchmark.scala ├── gifs/ │ ├── Dockerfile │ ├── README.md │ ├── create_missing.sc │ ├── demo-magic.sh │ ├── demo-no-magic.sh │ ├── example.sh │ ├── run_scenario.sh │ ├── scenarios/ │ │ ├── complete-install.sh │ │ ├── defaults.sh │ │ ├── demo.sh │ │ ├── education.sh │ │ ├── embeddable_scripts.sh │ │ ├── fast-scripts.sh │ │ ├── learning_curve.sh │ │ ├── powerful_scripts.sh │ │ ├── projects.sh │ │ ├── prototyping.sh │ │ ├── scripting.sh │ │ ├── self-contained-examples.sh │ │ ├── todo.sh │ │ ├── universal_tool.sh │ │ └── versions.sh │ └── svg_render/ │ ├── Dockerfile │ └── README.md ├── mill ├── mill.bat ├── millw ├── modules/ │ ├── build/ │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── scala/ │ │ │ │ └── build/ │ │ │ │ └── internal/ │ │ │ │ ├── Chdir.java │ │ │ │ ├── ChdirGraalvm.java │ │ │ │ ├── GraalvmUnistdExtras.java │ │ │ │ └── JavaParserProxyMakerSubst.java │ │ │ └── scala/ │ │ │ └── scala/ │ │ │ └── build/ │ │ │ ├── Bloop.scala │ │ │ ├── BloopBuildClient.scala │ │ │ ├── Build.scala │ │ │ ├── BuildThreads.scala │ │ │ ├── Builds.scala │ │ │ ├── CollectionOps.scala │ │ │ ├── ConsoleBloopBuildClient.scala │ │ │ ├── CrossBuildParams.scala │ │ │ ├── CrossKey.scala │ │ │ ├── CrossSources.scala │ │ │ ├── Directories.scala │ │ │ ├── GeneratedSource.scala │ │ │ ├── LocalRepo.scala │ │ │ ├── PersistentDiagnosticLogger.scala │ │ │ ├── Project.scala │ │ │ ├── ReplArtifacts.scala │ │ │ ├── ScalaCompilerParams.scala │ │ │ ├── ScalafixArtifacts.scala │ │ │ ├── ScopedSources.scala │ │ │ ├── Sources.scala │ │ │ ├── bsp/ │ │ │ │ ├── BloopSession.scala │ │ │ │ ├── Bsp.scala │ │ │ │ ├── BspClient.scala │ │ │ │ ├── BspImpl.scala │ │ │ │ ├── BspReloadableOptions.scala │ │ │ │ ├── BspServer.scala │ │ │ │ ├── BspThreads.scala │ │ │ │ ├── BuildClientForwardStubs.scala │ │ │ │ ├── BuildServerForwardStubs.scala │ │ │ │ ├── BuildServerProxy.scala │ │ │ │ ├── HasGeneratedSources.scala │ │ │ │ ├── HasGeneratedSourcesImpl.scala │ │ │ │ ├── IdeInputs.scala │ │ │ │ ├── JavaBuildServerForwardStubs.scala │ │ │ │ ├── JsonRpcErrorCodes.scala │ │ │ │ ├── JvmBuildServerForwardStubs.scala │ │ │ │ ├── LoggingBuildClient.scala │ │ │ │ ├── LoggingBuildServer.scala │ │ │ │ ├── LoggingBuildServerAll.scala │ │ │ │ ├── LoggingJavaBuildServer.scala │ │ │ │ ├── LoggingJvmBuildServer.scala │ │ │ │ ├── LoggingScalaBuildServer.scala │ │ │ │ ├── ScalaBuildServerForwardStubs.scala │ │ │ │ ├── package.scala │ │ │ │ └── protocol/ │ │ │ │ └── TextEdit.scala │ │ │ ├── compiler/ │ │ │ │ ├── BloopCompiler.scala │ │ │ │ ├── BloopCompilerMaker.scala │ │ │ │ ├── ScalaCompiler.scala │ │ │ │ ├── ScalaCompilerMaker.scala │ │ │ │ ├── SimpleJavaCompiler.scala │ │ │ │ ├── SimpleScalaCompiler.scala │ │ │ │ └── SimpleScalaCompilerMaker.scala │ │ │ ├── input/ │ │ │ │ ├── Element.scala │ │ │ │ ├── ElementsUtils.scala │ │ │ │ ├── Inputs.scala │ │ │ │ ├── ScalaCliInvokeData.scala │ │ │ │ └── WorkspaceOrigin.scala │ │ │ ├── internal/ │ │ │ │ ├── AmmUtil.scala │ │ │ │ ├── AppCodeWrapper.scala │ │ │ │ ├── ClassCodeWrapper.scala │ │ │ │ ├── JavaParserProxy.scala │ │ │ │ ├── JavaParserProxyBinary.scala │ │ │ │ ├── JavaParserProxyJvm.scala │ │ │ │ ├── JavaParserProxyMaker.scala │ │ │ │ ├── MainClass.scala │ │ │ │ ├── ManifestJar.scala │ │ │ │ ├── ObjectCodeWrapper.scala │ │ │ │ ├── Runner.scala │ │ │ │ ├── WrapperUtils.scala │ │ │ │ ├── markdown/ │ │ │ │ │ ├── MarkdownCodeBlock.scala │ │ │ │ │ ├── MarkdownCodeWrapper.scala │ │ │ │ │ └── MarkdownOpenFence.scala │ │ │ │ ├── resource/ │ │ │ │ │ ├── NativeResourceMapper.scala │ │ │ │ │ └── ResourceMapper.scala │ │ │ │ ├── util/ │ │ │ │ │ ├── RegexUtils.scala │ │ │ │ │ └── WarningMessages.scala │ │ │ │ └── zip/ │ │ │ │ └── WrappedZipInputStream.scala │ │ │ ├── package.scala │ │ │ ├── postprocessing/ │ │ │ │ ├── AsmPositionUpdater.scala │ │ │ │ ├── ByteCodePostProcessor.scala │ │ │ │ ├── LineConversion.scala │ │ │ │ ├── PostProcessor.scala │ │ │ │ ├── SemanticDbPostProcessor.scala │ │ │ │ ├── SemanticdbProcessor.scala │ │ │ │ └── TastyPostProcessor.scala │ │ │ └── preprocessing/ │ │ │ ├── CustomDirectivesReporter.scala │ │ │ ├── DataPreprocessor.scala │ │ │ ├── DeprecatedDirectives.scala │ │ │ ├── DirectivesPreprocessor.scala │ │ │ ├── ExtractedDirectives.scala │ │ │ ├── JarPreprocessor.scala │ │ │ ├── JavaPreprocessor.scala │ │ │ ├── MarkdownCodeBlockProcessor.scala │ │ │ ├── MarkdownPreprocessor.scala │ │ │ ├── PreprocessedMarkdown.scala │ │ │ ├── PreprocessedSource.scala │ │ │ ├── PreprocessingUtil.scala │ │ │ ├── Preprocessor.scala │ │ │ ├── ScalaPreprocessor.scala │ │ │ ├── ScriptPreprocessor.scala │ │ │ ├── SheBang.scala │ │ │ ├── UsingDirectivesOps.scala │ │ │ └── directives/ │ │ │ ├── DirectivesPreprocessingUtils.scala │ │ │ ├── PartiallyProcessedDirectives.scala │ │ │ └── PreprocessedDirectives.scala │ │ └── test/ │ │ └── scala/ │ │ └── scala/ │ │ └── build/ │ │ ├── options/ │ │ │ └── publish/ │ │ │ ├── ComputeVersionTests.scala │ │ │ └── VcsParseTest.scala │ │ └── tests/ │ │ ├── ActionableDiagnosticTests.scala │ │ ├── BspServerTests.scala │ │ ├── BuildOptionsTests.scala │ │ ├── BuildProjectTests.scala │ │ ├── BuildTests.scala │ │ ├── BuildTestsBloop.scala │ │ ├── BuildTestsScalac.scala │ │ ├── DirectiveTests.scala │ │ ├── DistinctByTests.scala │ │ ├── ExcludeTests.scala │ │ ├── FrameworkDiscoveryTests.scala │ │ ├── InputsTests.scala │ │ ├── JavaTestRunnerTests.scala │ │ ├── OfflineTests.scala │ │ ├── PackagingUsingDirectiveTests.scala │ │ ├── PreprocessingTests.scala │ │ ├── ReplArtifactsTests.scala │ │ ├── ScalaNativeUsingDirectiveTests.scala │ │ ├── ScalaPreprocessorTests.scala │ │ ├── ScriptWrapperTests.scala │ │ ├── SourceGeneratorTests.scala │ │ ├── SourcesTests.scala │ │ ├── TestInputs.scala │ │ ├── TestLogger.scala │ │ ├── TestUtil.scala │ │ ├── markdown/ │ │ │ ├── MarkdownCodeBlockTests.scala │ │ │ ├── MarkdownCodeWrapperTests.scala │ │ │ └── MarkdownTestUtil.scala │ │ └── util/ │ │ └── BloopServer.scala │ ├── build-macros/ │ │ └── src/ │ │ ├── main/ │ │ │ └── scala/ │ │ │ └── scala/ │ │ │ └── build/ │ │ │ ├── EitherCps.scala │ │ │ ├── EitherSequence.scala │ │ │ └── Ops.scala │ │ ├── negative-tests/ │ │ │ └── MismatchedLeft.scala │ │ └── test/ │ │ └── scala/ │ │ └── scala/ │ │ └── build/ │ │ └── CPSTest.scala │ ├── cli/ │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── scala/ │ │ │ │ └── cli/ │ │ │ │ ├── commands/ │ │ │ │ │ ├── pgp/ │ │ │ │ │ │ └── PgpCommandsSubst.java │ │ │ │ │ └── publish/ │ │ │ │ │ └── PgpProxyMakerSubst.java │ │ │ │ └── internal/ │ │ │ │ ├── Argv0Subst.java │ │ │ │ ├── Argv0SubstWindows.java │ │ │ │ ├── BouncycastleSignerMakerSubst.java │ │ │ │ ├── CsJniUtilsFeature.java │ │ │ │ ├── LibsodiumjniFeature.java │ │ │ │ ├── PPrintStringPrefixSubst.java │ │ │ │ └── PidSubst.java │ │ │ ├── resources/ │ │ │ │ └── META-INF/ │ │ │ │ └── native-image/ │ │ │ │ ├── extras/ │ │ │ │ │ ├── coursier/ │ │ │ │ │ │ └── reflect-config.json │ │ │ │ │ └── pprint/ │ │ │ │ │ └── reflect-config.json │ │ │ │ └── org.virtuslab/ │ │ │ │ └── scala-cli-core/ │ │ │ │ ├── jni-config.json │ │ │ │ ├── native-image.properties │ │ │ │ ├── proxy-config.json │ │ │ │ ├── reflect-config.json │ │ │ │ └── resource-config.json │ │ │ └── scala/ │ │ │ ├── coursier/ │ │ │ │ └── CoursierUtil.scala │ │ │ └── scala/ │ │ │ └── cli/ │ │ │ ├── CurrentParams.scala │ │ │ ├── ScalaCli.scala │ │ │ ├── ScalaCliCommands.scala │ │ │ ├── commands/ │ │ │ │ ├── CommandUtils.scala │ │ │ │ ├── CustomWindowsEnvVarUpdater.scala │ │ │ │ ├── HelpCmd.scala │ │ │ │ ├── NeedsArgvCommand.scala │ │ │ │ ├── OptionsHelper.scala │ │ │ │ ├── RestrictableCommand.scala │ │ │ │ ├── RestrictedCommandsParser.scala │ │ │ │ ├── ScalaCommand.scala │ │ │ │ ├── ScalaCommandWithCustomHelp.scala │ │ │ │ ├── ScalaVersions.scala │ │ │ │ ├── WatchUtil.scala │ │ │ │ ├── addpath/ │ │ │ │ │ ├── AddPath.scala │ │ │ │ │ └── AddPathOptions.scala │ │ │ │ ├── bloop/ │ │ │ │ │ ├── Bloop.scala │ │ │ │ │ ├── BloopExit.scala │ │ │ │ │ ├── BloopExitOptions.scala │ │ │ │ │ ├── BloopJson.scala │ │ │ │ │ ├── BloopOptions.scala │ │ │ │ │ ├── BloopOutput.scala │ │ │ │ │ ├── BloopOutputOptions.scala │ │ │ │ │ ├── BloopStart.scala │ │ │ │ │ └── BloopStartOptions.scala │ │ │ │ ├── bsp/ │ │ │ │ │ ├── Bsp.scala │ │ │ │ │ └── BspOptions.scala │ │ │ │ ├── clean/ │ │ │ │ │ ├── Clean.scala │ │ │ │ │ └── CleanOptions.scala │ │ │ │ ├── compile/ │ │ │ │ │ ├── Compile.scala │ │ │ │ │ └── CompileOptions.scala │ │ │ │ ├── config/ │ │ │ │ │ ├── Config.scala │ │ │ │ │ ├── ConfigOptions.scala │ │ │ │ │ └── ThrowawayPgpSecret.scala │ │ │ │ ├── default/ │ │ │ │ │ ├── Default.scala │ │ │ │ │ ├── DefaultFile.scala │ │ │ │ │ ├── DefaultFileOptions.scala │ │ │ │ │ ├── DefaultOptions.scala │ │ │ │ │ └── LegacyScalaOptions.scala │ │ │ │ ├── dependencyupdate/ │ │ │ │ │ ├── DependencyUpdate.scala │ │ │ │ │ └── DependencyUpdateOptions.scala │ │ │ │ ├── directories/ │ │ │ │ │ ├── Directories.scala │ │ │ │ │ └── DirectoriesOptions.scala │ │ │ │ ├── doc/ │ │ │ │ │ ├── Doc.scala │ │ │ │ │ └── DocOptions.scala │ │ │ │ ├── export0/ │ │ │ │ │ ├── Export.scala │ │ │ │ │ └── ExportOptions.scala │ │ │ │ ├── fix/ │ │ │ │ │ ├── BuiltInRules.scala │ │ │ │ │ ├── Fix.scala │ │ │ │ │ ├── FixOptions.scala │ │ │ │ │ ├── ScalafixOptions.scala │ │ │ │ │ └── ScalafixRules.scala │ │ │ │ ├── fmt/ │ │ │ │ │ ├── Fmt.scala │ │ │ │ │ ├── FmtOptions.scala │ │ │ │ │ └── FmtUtil.scala │ │ │ │ ├── github/ │ │ │ │ │ ├── GitHubApi.scala │ │ │ │ │ ├── HasSharedSecretOptions.scala │ │ │ │ │ ├── LibSodiumJni.scala │ │ │ │ │ ├── SecretCreate.scala │ │ │ │ │ ├── SecretCreateOptions.scala │ │ │ │ │ ├── SecretList.scala │ │ │ │ │ ├── SecretListOptions.scala │ │ │ │ │ └── SharedSecretOptions.scala │ │ │ │ ├── installcompletions/ │ │ │ │ │ ├── InstallCompletions.scala │ │ │ │ │ └── InstallCompletionsOptions.scala │ │ │ │ ├── installhome/ │ │ │ │ │ ├── InstallHome.scala │ │ │ │ │ └── InstallHomeOptions.scala │ │ │ │ ├── new/ │ │ │ │ │ ├── New.scala │ │ │ │ │ └── NewOptions.scala │ │ │ │ ├── package0/ │ │ │ │ │ ├── Package.scala │ │ │ │ │ ├── PackageOptions.scala │ │ │ │ │ └── PackagerOptions.scala │ │ │ │ ├── packaging/ │ │ │ │ │ └── Spark.scala │ │ │ │ ├── pgp/ │ │ │ │ │ ├── DummyOptions.scala │ │ │ │ │ ├── ExternalCommand.scala │ │ │ │ │ ├── KeyServer.scala │ │ │ │ │ ├── PgpCommand.scala │ │ │ │ │ ├── PgpCommandNames.scala │ │ │ │ │ ├── PgpCommands.scala │ │ │ │ │ ├── PgpCreate.scala │ │ │ │ │ ├── PgpCreateExternal.scala │ │ │ │ │ ├── PgpExternalCommand.scala │ │ │ │ │ ├── PgpExternalOptions.scala │ │ │ │ │ ├── PgpKeyId.scala │ │ │ │ │ ├── PgpKeyIdExternal.scala │ │ │ │ │ ├── PgpProxy.scala │ │ │ │ │ ├── PgpProxyJvm.scala │ │ │ │ │ ├── PgpProxyMaker.scala │ │ │ │ │ ├── PgpPull.scala │ │ │ │ │ ├── PgpPullOptions.scala │ │ │ │ │ ├── PgpPush.scala │ │ │ │ │ ├── PgpPushOptions.scala │ │ │ │ │ ├── PgpScalaSigningOptions.scala │ │ │ │ │ ├── PgpSign.scala │ │ │ │ │ ├── PgpSignExternal.scala │ │ │ │ │ ├── PgpVerify.scala │ │ │ │ │ ├── PgpVerifyExternal.scala │ │ │ │ │ └── SharedPgpPushPullOptions.scala │ │ │ │ ├── publish/ │ │ │ │ │ ├── ConfigUtil.scala │ │ │ │ │ ├── GitRepo.scala │ │ │ │ │ ├── GitRepoError.scala │ │ │ │ │ ├── Ivy.scala │ │ │ │ │ ├── OptionCheck.scala │ │ │ │ │ ├── OptionChecks.scala │ │ │ │ │ ├── Publish.scala │ │ │ │ │ ├── PublishConnectionOptions.scala │ │ │ │ │ ├── PublishLocal.scala │ │ │ │ │ ├── PublishLocalOptions.scala │ │ │ │ │ ├── PublishOptions.scala │ │ │ │ │ ├── PublishParamsOptions.scala │ │ │ │ │ ├── PublishRepositoryOptions.scala │ │ │ │ │ ├── PublishSetup.scala │ │ │ │ │ ├── PublishSetupOptions.scala │ │ │ │ │ ├── PublishUtils.scala │ │ │ │ │ ├── RepoParams.scala │ │ │ │ │ ├── RepositoryParser.scala │ │ │ │ │ ├── SetSecret.scala │ │ │ │ │ ├── SharedPublishOptions.scala │ │ │ │ │ └── checks/ │ │ │ │ │ ├── CheckUtils.scala │ │ │ │ │ ├── ComputeVersionCheck.scala │ │ │ │ │ ├── DeveloperCheck.scala │ │ │ │ │ ├── LicenseCheck.scala │ │ │ │ │ ├── NameCheck.scala │ │ │ │ │ ├── OrganizationCheck.scala │ │ │ │ │ ├── PasswordCheck.scala │ │ │ │ │ ├── PgpSecretKeyCheck.scala │ │ │ │ │ ├── RepositoryCheck.scala │ │ │ │ │ ├── ScmCheck.scala │ │ │ │ │ ├── UrlCheck.scala │ │ │ │ │ └── UserCheck.scala │ │ │ │ ├── repl/ │ │ │ │ │ ├── Repl.scala │ │ │ │ │ ├── ReplOptions.scala │ │ │ │ │ └── SharedReplOptions.scala │ │ │ │ ├── run/ │ │ │ │ │ ├── Run.scala │ │ │ │ │ ├── RunMode.scala │ │ │ │ │ ├── RunOptions.scala │ │ │ │ │ └── SharedRunOptions.scala │ │ │ │ ├── setupide/ │ │ │ │ │ ├── SetupIde.scala │ │ │ │ │ └── SetupIdeOptions.scala │ │ │ │ ├── shared/ │ │ │ │ │ ├── AllExternalHelpOptions.scala │ │ │ │ │ ├── ArgSplitter.scala │ │ │ │ │ ├── BenchmarkingOptions.scala │ │ │ │ │ ├── CoursierOptions.scala │ │ │ │ │ ├── CrossOptions.scala │ │ │ │ │ ├── GlobalOptions.scala │ │ │ │ │ ├── GlobalSuppressWarningOptions.scala │ │ │ │ │ ├── HasGlobalOptions.scala │ │ │ │ │ ├── HasSharedOptions.scala │ │ │ │ │ ├── HasSharedWatchOptions.scala │ │ │ │ │ ├── HelpGroupOptions.scala │ │ │ │ │ ├── HelpGroups.scala │ │ │ │ │ ├── HelpMessages.scala │ │ │ │ │ ├── HelpOptions.scala │ │ │ │ │ ├── JavaPropOptions.scala │ │ │ │ │ ├── LoggingOptions.scala │ │ │ │ │ ├── MainClassOptions.scala │ │ │ │ │ ├── MarkdownOptions.scala │ │ │ │ │ ├── ScalaCliHelp.scala │ │ │ │ │ ├── ScalaJsOptions.scala │ │ │ │ │ ├── ScalaNativeOptions.scala │ │ │ │ │ ├── ScalacExtraOptions.scala │ │ │ │ │ ├── ScalacOptions.scala │ │ │ │ │ ├── ScopeOptions.scala │ │ │ │ │ ├── SemanticDbOptions.scala │ │ │ │ │ ├── SharedBspFileOptions.scala │ │ │ │ │ ├── SharedCompilationServerOptions.scala │ │ │ │ │ ├── SharedDebugOptions.scala │ │ │ │ │ ├── SharedDependencyOptions.scala │ │ │ │ │ ├── SharedInputOptions.scala │ │ │ │ │ ├── SharedJavaOptions.scala │ │ │ │ │ ├── SharedJvmOptions.scala │ │ │ │ │ ├── SharedOptions.scala │ │ │ │ │ ├── SharedPythonOptions.scala │ │ │ │ │ ├── SharedVersionOptions.scala │ │ │ │ │ ├── SharedWatchOptions.scala │ │ │ │ │ ├── SharedWorkspaceOptions.scala │ │ │ │ │ ├── SnippetOptions.scala │ │ │ │ │ ├── SourceGeneratorOptions.scala │ │ │ │ │ ├── SuppressWarningOptions.scala │ │ │ │ │ └── VerbosityOptions.scala │ │ │ │ ├── shebang/ │ │ │ │ │ ├── Shebang.scala │ │ │ │ │ └── ShebangOptions.scala │ │ │ │ ├── test/ │ │ │ │ │ ├── Test.scala │ │ │ │ │ └── TestOptions.scala │ │ │ │ ├── uninstall/ │ │ │ │ │ ├── Uninstall.scala │ │ │ │ │ └── UninstallOptions.scala │ │ │ │ ├── uninstallcompletions/ │ │ │ │ │ ├── SharedUninstallCompletionsOptions.scala │ │ │ │ │ ├── UninstallCompletions.scala │ │ │ │ │ └── UninstallCompletionsOptions.scala │ │ │ │ ├── update/ │ │ │ │ │ ├── Update.scala │ │ │ │ │ └── UpdateOptions.scala │ │ │ │ ├── util/ │ │ │ │ │ ├── BuildCommandHelpers.scala │ │ │ │ │ ├── CommandHelpers.scala │ │ │ │ │ ├── HelpUtils.scala │ │ │ │ │ ├── JvmUtils.scala │ │ │ │ │ ├── RunHadoop.scala │ │ │ │ │ ├── RunSpark.scala │ │ │ │ │ ├── ScalaCliSttpBackend.scala │ │ │ │ │ └── ScalacOptionsUtil.scala │ │ │ │ └── version/ │ │ │ │ ├── Version.scala │ │ │ │ └── VersionOptions.scala │ │ │ ├── errors/ │ │ │ │ ├── FailedToSignFileError.scala │ │ │ │ ├── FoundVirtualInputsError.scala │ │ │ │ ├── GitHubApiError.scala │ │ │ │ ├── GraalVMNativeImageError.scala │ │ │ │ ├── InvalidSonatypePublishCredentials.scala │ │ │ │ ├── MalformedChecksumsError.scala │ │ │ │ ├── MalformedOptionError.scala │ │ │ │ ├── MissingConfigEntryError.scala │ │ │ │ ├── MissingPublishOptionError.scala │ │ │ │ ├── PgpError.scala │ │ │ │ ├── ScalaJsLinkingError.scala │ │ │ │ ├── ScaladocGenerationFailedError.scala │ │ │ │ ├── UploadError.scala │ │ │ │ └── WrongSonatypeServerError.scala │ │ │ ├── exportCmd/ │ │ │ │ ├── JsonProject.scala │ │ │ │ ├── JsonProjectDescriptor.scala │ │ │ │ ├── MavenProject.scala │ │ │ │ ├── MavenProjectDescriptor.scala │ │ │ │ ├── MillProject.scala │ │ │ │ ├── MillProjectDescriptor.scala │ │ │ │ ├── Project.scala │ │ │ │ ├── ProjectDescriptor.scala │ │ │ │ ├── SbtProject.scala │ │ │ │ └── SbtProjectDescriptor.scala │ │ │ ├── internal/ │ │ │ │ ├── Argv0.scala │ │ │ │ ├── CachedBinary.scala │ │ │ │ ├── CliLogger.scala │ │ │ │ ├── PPrintStringPrefixHelper.scala │ │ │ │ ├── Pid.scala │ │ │ │ ├── ProcUtil.scala │ │ │ │ ├── ProfileFileUpdater.scala │ │ │ │ └── ScalaJsLinker.scala │ │ │ ├── javaLauncher/ │ │ │ │ └── JavaLauncherCli.scala │ │ │ ├── launcher/ │ │ │ │ ├── LauncherCli.scala │ │ │ │ ├── LauncherOptions.scala │ │ │ │ ├── PowerOptions.scala │ │ │ │ └── ScalaRunnerLauncherOptions.scala │ │ │ ├── packaging/ │ │ │ │ ├── Library.scala │ │ │ │ └── NativeImage.scala │ │ │ ├── publish/ │ │ │ │ ├── BouncycastleExternalSigner.scala │ │ │ │ └── BouncycastleSignerMaker.scala │ │ │ └── util/ │ │ │ ├── ArgHelpers.scala │ │ │ ├── ArgParsers.scala │ │ │ ├── ConfigDbUtils.scala │ │ │ ├── ConfigPasswordOptionHelpers.scala │ │ │ ├── MaybeConfigPasswordOption.scala │ │ │ └── SeqHelpers.scala │ │ └── test/ │ │ └── scala/ │ │ ├── cli/ │ │ │ ├── commands/ │ │ │ │ └── tests/ │ │ │ │ ├── DocTests.scala │ │ │ │ ├── ReplOptionsTests.scala │ │ │ │ └── RunOptionsTests.scala │ │ │ └── tests/ │ │ │ ├── ArgSplitterTest.scala │ │ │ ├── CachedBinaryTests.scala │ │ │ ├── HelpCheck.scala │ │ │ ├── LauncherCliTest.scala │ │ │ ├── OptionsCheck.scala │ │ │ ├── PackageTests.scala │ │ │ ├── ScalafmtTests.scala │ │ │ ├── SetupScalaCLITests.scala │ │ │ └── TestUtil.scala │ │ └── scala/ │ │ └── cli/ │ │ ├── commands/ │ │ │ └── publish/ │ │ │ └── IvyTests.scala │ │ └── tests/ │ │ └── ScalacOptionsPrintTest.scala │ ├── config/ │ │ └── src/ │ │ └── main/ │ │ └── scala/ │ │ └── scala/ │ │ └── cli/ │ │ └── config/ │ │ ├── ConfigDb.scala │ │ ├── CredentialsValue.scala │ │ ├── ErrorMessages.scala │ │ ├── Key.scala │ │ ├── Keys.scala │ │ ├── PasswordOption.scala │ │ ├── PublishCredentials.scala │ │ ├── RawJson.scala │ │ ├── RepositoryCredentials.scala │ │ ├── Secret.scala │ │ └── internal/ │ │ └── JavaHelper.scala │ ├── core/ │ │ └── src/ │ │ └── main/ │ │ └── scala/ │ │ └── scala/ │ │ └── build/ │ │ ├── CsUtils.scala │ │ ├── Logger.scala │ │ ├── Os.scala │ │ ├── Position.scala │ │ ├── RepositoryUtils.scala │ │ ├── errors/ │ │ │ ├── AmbiguousPlatformError.scala │ │ │ ├── BuildException.scala │ │ │ ├── BuildInfoGenerationError.scala │ │ │ ├── CantDownloadAmmoniteError.scala │ │ │ ├── CheckScalaCliVersionError.scala │ │ │ ├── CompositeBuildException.scala │ │ │ ├── ConfigDbException.scala │ │ │ ├── CoursierDependencyError.scala │ │ │ ├── DependencyFormatError.scala │ │ │ ├── Diagnostic.scala │ │ │ ├── DirectiveErrors.scala │ │ │ ├── ExcludeDefinitionError.scala │ │ │ ├── FetchingDependenciesError.scala │ │ │ ├── FileNotFoundException.scala │ │ │ ├── ForbiddenPathReferenceError.scala │ │ │ ├── InputsException.scala │ │ │ ├── InvalidBinaryScalaVersionError.scala │ │ │ ├── JmhBuildFailedError.scala │ │ │ ├── JvmDownloadError.scala │ │ │ ├── MainClassError.scala │ │ │ ├── MalformedCliInputError.scala │ │ │ ├── MalformedDirectiveError.scala │ │ │ ├── MalformedInputError.scala │ │ │ ├── MalformedPlatformError.scala │ │ │ ├── MarkdownUnclosedBackticksError.scala │ │ │ ├── ModuleFormatError.scala │ │ │ ├── MultipleScalaVersionsError.scala │ │ │ ├── NoDocBuildError.scala │ │ │ ├── NoFrameworkFoundByBridgeError.scala │ │ │ ├── NoFrameworkFoundByNativeBridgeError.scala │ │ │ ├── NoMainClassFoundError.scala │ │ │ ├── NoScalaVersionProvidedError.scala │ │ │ ├── NoTestFrameworkFoundError.scala │ │ │ ├── NoTestFrameworkValueProvidedError.scala │ │ │ ├── NoTestsRun.scala │ │ │ ├── NoValidScalaVersionFoundError.scala │ │ │ ├── NoValueProvidedError.scala │ │ │ ├── NodeNotFoundError.scala │ │ │ ├── ParsingInputsException.scala │ │ │ ├── RepositoryFormatError.scala │ │ │ ├── ScalaNativeBuildError.scala │ │ │ ├── ScalaNativeCompatibilityError.scala │ │ │ ├── ScalaVersionError.scala │ │ │ ├── ScalafixPropertiesError.scala │ │ │ ├── SeveralMainClassesFoundError.scala │ │ │ ├── Severity.scala │ │ │ ├── TestError.scala │ │ │ ├── TooManyFrameworksFoundByBridgeError.scala │ │ │ ├── ToolkitVersionError.scala │ │ │ ├── UnexpectedDirectiveError.scala │ │ │ ├── UnexpectedJvmPlatformVersionError.scala │ │ │ ├── UnrecognizedDebugModeError.scala │ │ │ ├── UnrecognizedJsOptModeError.scala │ │ │ ├── UnsupportedFeatureError.scala │ │ │ ├── UnsupportedGradleModuleVariantError.scala │ │ │ ├── UnsupportedScalaVersionError.scala │ │ │ ├── UnusedDirectiveError.scala │ │ │ └── WorkspaceError.scala │ │ ├── internals/ │ │ │ ├── CodeWrapper.scala │ │ │ ├── ConsoleUtils.scala │ │ │ ├── CsLoggerUtil.scala │ │ │ ├── CustomProgressBarRefreshDisplay.scala │ │ │ ├── EnvVar.scala │ │ │ ├── FeatureType.scala │ │ │ ├── License.scala │ │ │ ├── Licenses.scala │ │ │ ├── Name.scala │ │ │ ├── NativeWrapper.scala │ │ │ ├── OsLibc.scala │ │ │ ├── Regexes.scala │ │ │ └── StableScalaVersion.scala │ │ └── warnings/ │ │ └── DeprecatedWarning.scala │ ├── directives/ │ │ └── src/ │ │ └── main/ │ │ └── scala/ │ │ └── scala/ │ │ └── build/ │ │ ├── directives/ │ │ │ ├── DirectiveDescription.scala │ │ │ ├── DirectiveExamples.scala │ │ │ ├── DirectiveGroupDetails.scala │ │ │ ├── DirectiveGroupName.scala │ │ │ ├── DirectiveLevel.scala │ │ │ ├── DirectiveName.scala │ │ │ ├── DirectiveSpecialSyntax.scala │ │ │ ├── DirectiveUsage.scala │ │ │ ├── DirectiveValueParser.scala │ │ │ ├── HasBuildOptions.scala │ │ │ ├── HasBuildOptionsWithRequirements.scala │ │ │ ├── HasBuildRequirements.scala │ │ │ └── ScopedValue.scala │ │ ├── errors/ │ │ │ ├── ScalaJsLinkingError.scala │ │ │ ├── SingleValueExpectedError.scala │ │ │ ├── UsingDirectiveExpectationError.scala │ │ │ ├── UsingFileFromUriError.scala │ │ │ ├── WrongDirectoryPathError.scala │ │ │ ├── WrongJarPathError.scala │ │ │ ├── WrongJavaHomePathError.scala │ │ │ └── WrongSourcePathError.scala │ │ └── preprocessing/ │ │ ├── ScopePath.scala │ │ ├── Scoped.scala │ │ └── directives/ │ │ ├── Benchmarking.scala │ │ ├── BuildInfo.scala │ │ ├── ClasspathUtils.scala │ │ ├── ComputeVersion.scala │ │ ├── CustomJar.scala │ │ ├── Dependency.scala │ │ ├── Directive.scala │ │ ├── DirectiveHandler.scala │ │ ├── DirectivePrefix.scala │ │ ├── DirectiveUtil.scala │ │ ├── Exclude.scala │ │ ├── JavaHome.scala │ │ ├── JavaOptions.scala │ │ ├── JavaProps.scala │ │ ├── JavacOptions.scala │ │ ├── Jvm.scala │ │ ├── MainClass.scala │ │ ├── ObjectWrapper.scala │ │ ├── Packaging.scala │ │ ├── Platform.scala │ │ ├── Plugin.scala │ │ ├── ProcessedDirective.scala │ │ ├── Publish.scala │ │ ├── PublishContextual.scala │ │ ├── Python.scala │ │ ├── Repository.scala │ │ ├── RequirePlatform.scala │ │ ├── RequireScalaVersion.scala │ │ ├── RequireScalaVersionBounds.scala │ │ ├── RequireScope.scala │ │ ├── Resources.scala │ │ ├── ScalaJs.scala │ │ ├── ScalaNative.scala │ │ ├── ScalaVersion.scala │ │ ├── ScalacOptions.scala │ │ ├── ScopedDirective.scala │ │ ├── Sources.scala │ │ ├── StrictDirective.scala │ │ ├── Tests.scala │ │ ├── Toolkit.scala │ │ └── Watching.scala │ ├── docs-tests/ │ │ ├── README.md │ │ └── src/ │ │ ├── main/ │ │ │ └── scala/ │ │ │ └── sclicheck/ │ │ │ └── sclicheck.scala │ │ └── test/ │ │ ├── resources/ │ │ │ └── test.md │ │ └── scala/ │ │ └── sclicheck/ │ │ ├── DocTests.scala │ │ ├── GifTests.scala │ │ ├── MarkdownLinkTests.scala │ │ ├── SclicheckTests.scala │ │ └── TestUtil.scala │ ├── dummy/ │ │ ├── amm/ │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── scala/ │ │ │ └── AmmDummy.scala │ │ └── scalafmt/ │ │ └── src/ │ │ └── main/ │ │ └── scala/ │ │ └── ScalafmtDummy.scala │ ├── generate-reference-doc/ │ │ └── src/ │ │ └── main/ │ │ └── scala/ │ │ └── scala/ │ │ └── cli/ │ │ └── doc/ │ │ ├── GenerateReferenceDoc.scala │ │ ├── InternalDocOptions.scala │ │ └── ReferenceDocUtils.scala │ ├── integration/ │ │ ├── docker/ │ │ │ └── src/ │ │ │ └── test/ │ │ │ └── scala/ │ │ │ └── scala/ │ │ │ └── cli/ │ │ │ └── integration/ │ │ │ └── RunDockerTests.scala │ │ └── src/ │ │ ├── main/ │ │ │ └── scala/ │ │ │ └── scala/ │ │ │ └── cli/ │ │ │ └── integration/ │ │ │ └── TestInputs.scala │ │ └── test/ │ │ ├── java/ │ │ │ └── scala/ │ │ │ └── cli/ │ │ │ └── integration/ │ │ │ └── bsp/ │ │ │ ├── WrappedSourceItem.java │ │ │ ├── WrappedSourcesItem.java │ │ │ └── WrappedSourcesResult.java │ │ ├── resources/ │ │ │ └── test-keys/ │ │ │ ├── key.asc │ │ │ └── key.skr │ │ └── scala/ │ │ └── scala/ │ │ └── cli/ │ │ └── integration/ │ │ ├── ArgsFileTests.scala │ │ ├── BloopTests.scala │ │ ├── BspSuite.scala │ │ ├── BspTestDefinitions.scala │ │ ├── BspTests212.scala │ │ ├── BspTests213.scala │ │ ├── BspTests2Definitions.scala │ │ ├── BspTests3Definitions.scala │ │ ├── BspTests3Lts.scala │ │ ├── BspTests3NextRc.scala │ │ ├── BspTestsDefault.scala │ │ ├── CleanTests.scala │ │ ├── CompileScalacCompatTestDefinitions.scala │ │ ├── CompileTestDefinitions.scala │ │ ├── CompileTests212.scala │ │ ├── CompileTests213.scala │ │ ├── CompileTests3Lts.scala │ │ ├── CompileTests3NextRc.scala │ │ ├── CompileTests3StableDefinitions.scala │ │ ├── CompileTestsDefault.scala │ │ ├── CompilerPluginTestDefinitions.scala │ │ ├── CompleteTests.scala │ │ ├── ConfigTests.scala │ │ ├── CoursierScalaInstallationTestHelper.scala │ │ ├── DefaultFileTests.scala │ │ ├── DefaultTests.scala │ │ ├── DependencyUpdateTests.scala │ │ ├── DirectoriesTests.scala │ │ ├── DocTestDefinitions.scala │ │ ├── DocTests212.scala │ │ ├── DocTests213.scala │ │ ├── DocTests3Lts.scala │ │ ├── DocTests3NextRc.scala │ │ ├── DocTestsDefault.scala │ │ ├── ExportCommonTestDefinitions.scala │ │ ├── ExportJsonTestDefinitions.scala │ │ ├── ExportJsonTestsDefault.scala │ │ ├── ExportMavenTest3NextRc.scala │ │ ├── ExportMavenTestDefinitions.scala │ │ ├── ExportMavenTestJava.scala │ │ ├── ExportMavenTests212.scala │ │ ├── ExportMavenTests213.scala │ │ ├── ExportMavenTests3Lts.scala │ │ ├── ExportMill012Tests212.scala │ │ ├── ExportMill012Tests213.scala │ │ ├── ExportMill012Tests3Lts.scala │ │ ├── ExportMill012Tests3NextRc.scala │ │ ├── ExportMill012TestsDefault.scala │ │ ├── ExportMill1Tests212.scala │ │ ├── ExportMill1Tests213.scala │ │ ├── ExportMill1Tests3Lts.scala │ │ ├── ExportMill1Tests3NextRc.scala │ │ ├── ExportMill1TestsDefault.scala │ │ ├── ExportMillTestDefinitions.scala │ │ ├── ExportSbtTestDefinitions.scala │ │ ├── ExportSbtTests212.scala │ │ ├── ExportSbtTests213.scala │ │ ├── ExportSbtTests3Lts.scala │ │ ├── ExportSbtTests3NextRc.scala │ │ ├── ExportSbtTestsDefault.scala │ │ ├── ExportScalaOrientedBuildToolsTestDefinitions.scala │ │ ├── ExportTestProjects.scala │ │ ├── FixBuiltInRulesTestDefinitions.scala │ │ ├── FixScalafixRulesTestDefinitions.scala │ │ ├── FixTestDefinitions.scala │ │ ├── FixTests212.scala │ │ ├── FixTests213.scala │ │ ├── FixTests3Lts.scala │ │ ├── FixTests3NextRc.scala │ │ ├── FixTestsDefault.scala │ │ ├── FmtTests.scala │ │ ├── GitHubTests.scala │ │ ├── HadoopTests.scala │ │ ├── HelpTests.scala │ │ ├── InstallAndUninstallCompletionsTests.scala │ │ ├── InstallHomeTests.scala │ │ ├── JmhSuite.scala │ │ ├── JmhTests.scala │ │ ├── LegacyScalaRunnerTestDefinitions.scala │ │ ├── LoggingTests.scala │ │ ├── MarkdownTests.scala │ │ ├── MavenTestHelper.scala │ │ ├── MetaCheck.scala │ │ ├── MillTestHelper.scala │ │ ├── NativePackagerTests.scala │ │ ├── NewTests.scala │ │ ├── PackageTestDefinitions.scala │ │ ├── PackageTests212.scala │ │ ├── PackageTests213.scala │ │ ├── PackageTests3Lts.scala │ │ ├── PackageTests3NextRc.scala │ │ ├── PackageTestsDefault.scala │ │ ├── PgpTests.scala │ │ ├── PublishLocalTestDefinitions.scala │ │ ├── PublishLocalTests212.scala │ │ ├── PublishLocalTests213.scala │ │ ├── PublishLocalTests3Lts.scala │ │ ├── PublishLocalTests3NextRc.scala │ │ ├── PublishLocalTestsDefault.scala │ │ ├── PublishSetupTests.scala │ │ ├── PublishTestDefinitions.scala │ │ ├── PublishTests212.scala │ │ ├── PublishTests213.scala │ │ ├── PublishTests3Lts.scala │ │ ├── PublishTests3NextRc.scala │ │ ├── PublishTestsDefault.scala │ │ ├── ReplAmmoniteTestDefinitions.scala │ │ ├── ReplAmmoniteTests3StableDefinitions.scala │ │ ├── ReplTestDefinitions.scala │ │ ├── ReplTests212.scala │ │ ├── ReplTests213.scala │ │ ├── ReplTests3Lts.scala │ │ ├── ReplTests3NextRc.scala │ │ ├── ReplTestsDefault.scala │ │ ├── RunGistTestDefinitions.scala │ │ ├── RunJdkTestDefinitions.scala │ │ ├── RunPipedSourcesTestDefinitions.scala │ │ ├── RunScalaJsTestDefinitions.scala │ │ ├── RunScalaNativeTestDefinitions.scala │ │ ├── RunScalaPyTestDefinitions.scala │ │ ├── RunScalacCompatTestDefinitions.scala │ │ ├── RunScriptTestDefinitions.scala │ │ ├── RunSnippetTestDefinitions.scala │ │ ├── RunTestDefinitions.scala │ │ ├── RunTests212.scala │ │ ├── RunTests213.scala │ │ ├── RunTests3Lts.scala │ │ ├── RunTests3NextRc.scala │ │ ├── RunTestsDefault.scala │ │ ├── RunWithWatchTestDefinitions.scala │ │ ├── RunZipTestDefinitions.scala │ │ ├── SbtTestHelper.scala │ │ ├── ScalaCliSuite.scala │ │ ├── ScriptWrapperTestDefinitions.scala │ │ ├── SemanticDbTestDefinitions.scala │ │ ├── SharedRunTests.scala │ │ ├── SipScalaTests.scala │ │ ├── SparkTestDefinitions.scala │ │ ├── SparkTests212.scala │ │ ├── SparkTests213.scala │ │ ├── TestBspClient.scala │ │ ├── TestNativeImageOnScala3.scala │ │ ├── TestScalaVersionArgs.scala │ │ ├── TestTestDefinitions.scala │ │ ├── TestTests212.scala │ │ ├── TestTests213.scala │ │ ├── TestTests3Lts.scala │ │ ├── TestTests3NextRc.scala │ │ ├── TestTestsDefault.scala │ │ ├── TestUtil.scala │ │ ├── UpdateTests.scala │ │ ├── VersionTests.scala │ │ ├── WithWarmUpScalaCliSuite.scala │ │ ├── package.scala │ │ └── util/ │ │ ├── BloopUtil.scala │ │ ├── CompilerPluginUtil.scala │ │ └── DockerServer.scala │ ├── java-test-runner/ │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── scala/ │ │ └── build/ │ │ └── testrunner/ │ │ ├── JavaAsmTestRunner.java │ │ ├── JavaDynamicTestRunner.java │ │ ├── JavaFrameworkUtils.java │ │ ├── JavaTestLogger.java │ │ └── JavaTestRunner.java │ ├── options/ │ │ └── src/ │ │ ├── main/ │ │ │ └── scala/ │ │ │ └── scala/ │ │ │ └── build/ │ │ │ ├── Artifacts.scala │ │ │ ├── CoursierUtils.scala │ │ │ ├── Positioned.scala │ │ │ ├── ScalaArtifacts.scala │ │ │ ├── TemporaryInMemoryRepository.scala │ │ │ ├── actionable/ │ │ │ │ ├── ActionableDependencyHandler.scala │ │ │ │ ├── ActionableDiagnostic.scala │ │ │ │ ├── ActionableHandler.scala │ │ │ │ ├── ActionablePreprocessor.scala │ │ │ │ └── errors/ │ │ │ │ └── ActionableHandlerError.scala │ │ │ ├── info/ │ │ │ │ ├── BuildInfo.scala │ │ │ │ └── ScopedBuildInfo.scala │ │ │ ├── interactive/ │ │ │ │ ├── Interactive.scala │ │ │ │ └── InteractiveFileOps.scala │ │ │ ├── internal/ │ │ │ │ ├── ExternalBinary.scala │ │ │ │ ├── ExternalBinaryParams.scala │ │ │ │ ├── FetchExternalBinary.scala │ │ │ │ ├── ScalaJsLinkerConfig.scala │ │ │ │ └── StdInConcurrentReader.scala │ │ │ ├── internals/ │ │ │ │ └── Util.scala │ │ │ └── options/ │ │ │ ├── BuildOptions.scala │ │ │ ├── BuildRequirements.scala │ │ │ ├── ClassPathOptions.scala │ │ │ ├── ComputeVersion.scala │ │ │ ├── ConfigMonoid.scala │ │ │ ├── HasHashData.scala │ │ │ ├── HasScope.scala │ │ │ ├── HashedType.scala │ │ │ ├── InternalDependenciesOptions.scala │ │ │ ├── InternalOptions.scala │ │ │ ├── JavaOpt.scala │ │ │ ├── JavaOptions.scala │ │ │ ├── JmhOptions.scala │ │ │ ├── MaybeScalaVersion.scala │ │ │ ├── PackageOptions.scala │ │ │ ├── PackageType.scala │ │ │ ├── Platform.scala │ │ │ ├── PostBuildOptions.scala │ │ │ ├── PublishContextualOptions.scala │ │ │ ├── PublishOptions.scala │ │ │ ├── ReplOptions.scala │ │ │ ├── SNNumeralVersion.scala │ │ │ ├── ScalaJsOptions.scala │ │ │ ├── ScalaNativeOptions.scala │ │ │ ├── ScalaOptions.scala │ │ │ ├── ScalaSigningCliOptions.scala │ │ │ ├── ScalaVersionUtil.scala │ │ │ ├── ScalacOpt.scala │ │ │ ├── Scope.scala │ │ │ ├── ScriptOptions.scala │ │ │ ├── SemanticDbOptions.scala │ │ │ ├── ShadowingSeq.scala │ │ │ ├── SourceGeneratorOptions.scala │ │ │ ├── SuppressWarningOptions.scala │ │ │ ├── TestOptions.scala │ │ │ ├── WatchOptions.scala │ │ │ ├── WithBuildRequirements.scala │ │ │ ├── packaging/ │ │ │ │ ├── DebianOptions.scala │ │ │ │ ├── DockerOptions.scala │ │ │ │ ├── NativeImageOptions.scala │ │ │ │ ├── RedHatOptions.scala │ │ │ │ └── WindowsOptions.scala │ │ │ ├── publish/ │ │ │ │ ├── ConfigPasswordOption.scala │ │ │ │ ├── Developer.scala │ │ │ │ ├── License.scala │ │ │ │ ├── Signer.scala │ │ │ │ └── Vcs.scala │ │ │ ├── scalajs/ │ │ │ │ └── ScalaJsLinkerOptions.scala │ │ │ └── validation/ │ │ │ └── BuildOptionsRule.scala │ │ └── test/ │ │ └── scala/ │ │ └── scala/ │ │ └── build/ │ │ └── options/ │ │ └── ConfigMonoidTest.scala │ ├── runner/ │ │ └── src/ │ │ └── main/ │ │ └── scala/ │ │ └── scala/ │ │ └── cli/ │ │ └── runner/ │ │ ├── Runner.scala │ │ └── StackTracePrinter.scala │ ├── scala-cli-bsp/ │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── scala/ │ │ └── build/ │ │ └── bsp/ │ │ ├── ScalaScriptBuildServer.java │ │ ├── WrappedSourceItem.java │ │ ├── WrappedSourcesItem.java │ │ ├── WrappedSourcesParams.java │ │ └── WrappedSourcesResult.java │ ├── scalaparse/ │ │ └── src/ │ │ └── main/ │ │ └── scala/ │ │ └── build/ │ │ └── internal/ │ │ ├── ImportTree.scala │ │ └── ScalaParse.scala │ ├── specification-level/ │ │ └── src/ │ │ └── main/ │ │ └── scala/ │ │ └── scala/ │ │ └── cli/ │ │ └── commands/ │ │ └── SpecificationLevel.scala │ ├── tasty-lib/ │ │ └── src/ │ │ └── main/ │ │ └── scala/ │ │ └── scala/ │ │ └── build/ │ │ └── tastylib/ │ │ ├── TastyBuffer.scala │ │ ├── TastyData.scala │ │ ├── TastyFormat.scala │ │ ├── TastyHeaderUnpickler.scala │ │ ├── TastyName.scala │ │ ├── TastyReader.scala │ │ ├── TastyUnpickler.scala │ │ ├── TastyVersions.scala │ │ └── UnpickleException.scala │ └── test-runner/ │ └── src/ │ └── main/ │ └── scala/ │ └── scala/ │ └── build/ │ └── testrunner/ │ ├── AsmTestRunner.scala │ ├── DynamicTestRunner.scala │ ├── FrameworkUtils.scala │ ├── Logger.scala │ └── TestRunner.scala ├── project/ │ ├── deps/ │ │ └── package.mill │ ├── musl-image/ │ │ ├── Dockerfile │ │ └── setup.sh │ ├── package.mill │ ├── publish/ │ │ └── package.mill │ ├── settings/ │ │ └── package.mill │ ├── utils/ │ │ └── package.mill │ └── website/ │ └── package.mill ├── scala-cli ├── scala-cli-src ├── scala-cli.bat ├── scala-cli.sh └── website/ ├── .gitignore ├── README.md ├── babel.config.js ├── docs/ │ ├── _advanced_install.mdx │ ├── _misc/ │ │ ├── _category_.json │ │ ├── benchmarking.md │ │ └── browse.md │ ├── _scala-ecosystem.md │ ├── commands/ │ │ ├── _category_.json │ │ ├── basics.md │ │ ├── clean.md │ │ ├── compile.md │ │ ├── completions.md │ │ ├── config.md │ │ ├── doc.md │ │ ├── export.md │ │ ├── fix.md │ │ ├── fmt.md │ │ ├── misc/ │ │ │ ├── _category_.json │ │ │ ├── bloop.md │ │ │ ├── default-file.md │ │ │ └── pgp.md │ │ ├── package.md │ │ ├── publishing/ │ │ │ ├── _category_.json │ │ │ ├── publish-local.md │ │ │ ├── publish-setup.md │ │ │ └── publish.md │ │ ├── repl.md │ │ ├── run.md │ │ ├── setup-ide.md │ │ ├── shebang.md │ │ ├── test.md │ │ └── version.md │ ├── cookbooks/ │ │ ├── _category_.json │ │ ├── ide/ │ │ │ ├── _category_.json │ │ │ ├── emacs.md │ │ │ ├── intellij-multi-bsp.md │ │ │ ├── intellij-sbt-with-bsp.md │ │ │ ├── intellij.md │ │ │ └── vscode.md │ │ ├── intro.md │ │ ├── introduction/ │ │ │ ├── _category_.json │ │ │ ├── debugging.md │ │ │ ├── formatting.md │ │ │ ├── gh-action.md │ │ │ ├── gists.md │ │ │ ├── instant-startup-scala-scripts.md │ │ │ ├── scala-jvm.md │ │ │ ├── scala-scripts.md │ │ │ ├── scala-versions.md │ │ │ ├── show-sources.md │ │ │ └── test-only.md │ │ └── package/ │ │ ├── _category_.json │ │ ├── native-images.md │ │ ├── scala-docker.md │ │ └── scala-package.md │ ├── getting_started.md │ ├── guides/ │ │ ├── _category_.json │ │ ├── advanced/ │ │ │ ├── _category_.json │ │ │ ├── custom-toolkit.md │ │ │ ├── internals.md │ │ │ ├── java-properties.md │ │ │ ├── piping.md │ │ │ ├── scala-js.md │ │ │ ├── scala-native.md │ │ │ ├── snippets.md │ │ │ └── verbosity.md │ │ ├── intro.md │ │ ├── introduction/ │ │ │ ├── _category_.json │ │ │ ├── configuration.md │ │ │ ├── dependencies.md │ │ │ ├── ide.md │ │ │ ├── old-runner-migration.md │ │ │ ├── toolkit.md │ │ │ ├── update-dependencies.md │ │ │ └── using-directives.md │ │ ├── power/ │ │ │ ├── _category_.json │ │ │ ├── markdown.md │ │ │ ├── offline.md │ │ │ ├── proxy.md │ │ │ ├── python.md │ │ │ ├── repositories.md │ │ │ └── sbt-mill.md │ │ └── scripting/ │ │ ├── _category_.json │ │ ├── scripts.md │ │ └── shebang.md │ ├── overview.md │ ├── reference/ │ │ ├── _category_.json │ │ ├── build-info.md │ │ ├── cli-options.md │ │ ├── commands.md │ │ ├── dependency.md │ │ ├── directives.md │ │ ├── env-vars.md │ │ ├── password-options.md │ │ ├── root-dir.md │ │ ├── scala-command/ │ │ │ ├── cli-options.md │ │ │ ├── commands.md │ │ │ ├── directives.md │ │ │ ├── env-vars.md │ │ │ ├── index.md │ │ │ └── runner-specification.md │ │ └── scala-versions.md │ ├── release_notes.md │ └── under-the-hood.md ├── docusaurus.config.js ├── package.json ├── safe-yarn.sh ├── sidebars.js ├── src/ │ ├── components/ │ │ ├── BasicInstall.js │ │ ├── BigHeader.js │ │ ├── DownloadButton.js │ │ ├── IconBox.js │ │ ├── ImageBox.js │ │ ├── Layouts.js │ │ ├── MarkdownComponents.js │ │ ├── Section.js │ │ ├── SectionAbout.js │ │ ├── SectionImageBox.js │ │ ├── SmallHeader.js │ │ ├── TitleSection.js │ │ ├── UseCase.js │ │ ├── UseCaseTile.js │ │ ├── YellowBanner.js │ │ ├── features.js │ │ └── osUtils.js │ ├── css/ │ │ ├── custom copy.css │ │ └── custom.css │ ├── pages/ │ │ ├── education.js │ │ ├── index.js │ │ ├── index.module.css │ │ ├── install.js │ │ ├── markdown-page.md │ │ ├── projects.js │ │ ├── prototyping.js │ │ ├── scripting.js │ │ └── spark.md │ ├── scss/ │ │ ├── _variables.scss │ │ ├── components/ │ │ │ ├── runnable-sample.scss │ │ │ ├── section-about.scss │ │ │ ├── section-base.scss │ │ │ ├── section-features.scss │ │ │ ├── section-image-box.scss │ │ │ ├── section-install-cli.scss │ │ │ ├── section-use-tiles.scss │ │ │ ├── section-yellow-banner.scss │ │ │ └── tooltip.scss │ │ └── style.scss │ └── theme/ │ └── Root.js └── static/ ├── .nojekyll └── CNAME ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ ./out .bloop .bsp .metals .scala-build .scala gifs website .github Dockerfile ================================================ FILE: .git-blame-ignore-revs ================================================ # Scala Steward: Reformat with scalafmt 3.7.3 34ae72e8cf5878dccb44ac3f864cbf4892f18354 # Scala Steward: Reformat with scalafmt 3.8.2 6d2639650f6e0b941840b995cc30b7de7afff5a0 # Scala Steward: Reformat with scalafmt 3.8.3 52b913a12d8abdff1b340db668ebe38c59b423e4 # Scala Steward: Reformat with scalafmt 3.8.5 74f069ccdaa91872cb77dc1f902752221d588db1 # Scala Steward: Reformat with scalafmt 3.10.7 f45699aa27d21bfe09e087a6d355ab6abf1ff0e6 ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: bug assignees: '' --- **Version(s)** Please provide the version(s) of Scala CLI that is affected by this bug **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Please provide us with steps on how to reproduce the bug. Code snippets (or link to the used codebase) and used commands are especially useful. **Expected behaviour** A clear and concise description of what you expected to happen. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: enhancement assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/ISSUE_TEMPLATE/new_release.md ================================================ --- name: Plan a release about: Plan releasing a new version title: "Release v[VERSION]" labels: internal assignees: '' --- **What version number is to be released?** v[VERSION] **What is the estimated date for this release?** [RELEASE DATE] **Release procedure** Please refer to the [Release Procedure doc](https://github.com/VirtusLab/scala-cli/blob/main/.github/release/release-procedure.md). **Release notes** Please remember to create a pull request with a draft of the release notes in the [Release Notes History doc](https://github.com/VirtusLab/scala-cli/blob/main/website/docs/release_notes.md). Please make sure the notes render correctly on [the website](https://scala-cli.virtuslab.org/docs/release_notes). That includes swapping out GitHub-idiomatic @mentions of users, links to PRs, issues, etc. You can do that using the [regexes provided in this doc](https://github.com/VirtusLab/scala-cli/blob/main/.github/release/release-notes-regexes.md) ================================================ FILE: .github/ISSUE_TEMPLATE/other.md ================================================ --- name: Other about: Request a change that is neither a new feature nor related to a bug. title: '' labels: '' assignees: '' --- **Version(s)** Please provide the version(s) of Scala CLI for which the proposed change is necessary (if at all relevant). **Describe what needs to be done and why** A clear and concise description of what you want to happen - and why. **Is your feature request related to a past ticket or discussion?** Please mention any other issues or discussions relevant to this one. **Describe alternatives you've considered** A clear and concise description of any alternative solutions you've considered. **Additional context** Add any other context or screenshots that might be relevant in this section. ================================================ FILE: .github/actions/windows-reg-import/action.yml ================================================ name: windows-reg-import description: Import a .reg file and verify those registry values (best-effort) inputs: reg-file: description: "Path to the .reg file" required: true runs: using: "composite" steps: - name: Attempt to import custom registry (best-effort) shell: pwsh run: | try { $regFile = Join-Path $env:GITHUB_WORKSPACE "${{ inputs.reg-file }}" if (-not (Test-Path $regFile)) { Write-Warning "Registry file not found (skipping): $regFile" } else { Write-Host "Importing registry from $regFile (attempting, non-fatal)" reg import $regFile 2>&1 | ForEach-Object { Write-Host $_ } } } catch { Write-Warning "Registry import failed (ignored): $_" } - name: Attempt to verify registry values (best-effort) shell: pwsh run: | try { $acp = (Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Nls\CodePage' -Name ACP -ErrorAction Stop).ACP Write-Host "ACP = $acp" } catch { Write-Warning "Failed to read ACP (ignored): $_" } try { $eb = (Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Nls\MUILanguagePreferences' -Name EnableBetaUnicode -ErrorAction Stop).EnableBetaUnicode Write-Host "EnableBetaUnicode = $eb" } catch { Write-Warning "Failed to read EnableBetaUnicode (ignored): $_" } ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" groups: github-actions: patterns: - "*" - package-ecosystem: "npm" directory: "/website" schedule: interval: "weekly" groups: npm-dependencies: patterns: - "*" ================================================ FILE: .github/pull_request_template.md ================================================ ## Checklist - [ ] tested the solution locally and it works - [ ] ran the code formatter (`scala-cli fmt .`) - [ ] ran `scalafix` (`./mill -i __.fix`) - [ ] ran reference docs auto-generation (`./mill -i 'generate-reference-doc[]'.run`) ## How much have your relied on LLM-based tools in this contribution? ## How was the solution tested? ## Additional notes ================================================ FILE: .github/release/release-notes-regexes.md ================================================ # Regexes for preparing Scala CLI release notes When auto-generating release notes from the GitHub UI, the notes will contain GitHub-idiomatic @mentions of users, links to PRs, issues, etc. Those have to then be swapped out for the corresponding Markdown syntax to be rendered correctly on our documentation website. What's worse, the GitHub syntax has to be preserved in the GitHub release description, as using the full Markdown syntax required by our website breaks the GitHub mouse hover magic. This has since been automated in the [process_release_notes.sc](../scripts/process_release_notes.sc) script. ```bash # Check if the release notes need processing .github/scripts/process_release_notes.sc check website/docs/release_notes.md # Error: File ~/scala-cli/website/docs/release_notes.md contains patterns that need transformation # The following patterns were found that should be transformed: # - Pattern: by @(.*?) in(?!.*\]\() # - Pattern: (? # Apply the regexes to fix the release notes .github/scripts/process_release_notes.sc apply website/docs/release_notes.md # Applied regexes to: ~/scala-cli/website/docs/release_notes.md # Verify that the release notes are now properly formatted # This is the check we run on the CI, as well .github/scripts/process_release_notes.sc verify website/docs/release_notes.md # File /~/scala-cli/website/docs/release_notes.md is properly formatted ``` If you ever need to manually fix the release notes, you can use the old regexes below. Do keep in mind that IDEA IntelliJ allows to automatically apply regexes when replacing text, so you can use that to fix the release notes on the fly. ![image](img/apply-regexes-on-release-notes-in-intellij.png) ## PR link Find: `in https\:\/\/github\.com\/VirtusLab\/scala\-cli\/pull\/(.*?)$`
Replace: `in [#$1](https://github.com/VirtusLab/scala-cli/pull/$1)` ## Contributor link Find: `by @(.*?) in`
Replace: `by [@$1](https://github.com/$1) in` ## New contributor link Find: `@(.*?) made`
Replace: `[@$1](https://github.com/$1) made` ## No GH contributor link Find: `by \[@(.*?).\(.*\) in`
Replace: `by @$1 in` ================================================ FILE: .github/release/release-procedure.md ================================================ # Release procedure reference - [ ] Draft release notes using the `Draft new release` button in the `Releases` section of `scala-cli` GitHub page. - [ ] Create a tag for the new release. - [ ] Use the `Auto-generate release notes` feature to pre-populate the document with pull requests included in the release. - [ ] Fill in the remaining sections, as in previous releases (features worth mentioning, notable changes, etc). - [ ] Don't publish, save as draft instead - [ ] Add the release notes on top of [the release notes doc](https://github.com/VirtusLab/scala-cli/blob/main/website/docs/release_notes.md) and create a PR. - [ ] Make sure the notes render correctly on [the website](https://scala-cli.virtuslab.org/docs/release_notes) - that includes swapping out GitHub-idiomatic @mentions of users, links to PRs, issues, etc. - This is automated with the [process_release_notes.sc](../scripts/process_release_notes.sc) script. - When using IntelliJ you can do that using the regexes in [release-notes-regexes.md](release-notes-regexes.md). - [ ] Copy any fixes over to the draft after getting the PR reviewed and merged. - [ ] Mark the release draft as `pre-release` and then `Publish Release` - [ ] Wait for a green release CI build with all the updated versions. - [ ] Double check if none of the steps failed, including individual distribution channels in the `update-packages` and `windows-packages` jobs. - [ ] ScalaCLI Setup - [ ] Merge pull request with updated Scala CLI version in [scala-cli-setup](https://github.com/VirtusLab/scala-cli-setup) repository. Pull request should be opened automatically after release. - [ ] Wait for the `Update dist` PR to be automatically created after the previous one has been merged, and then proceed to merge it. - [ ] Make a release with the updated Scala CLI version. - [ ] Update the `v1` & `v1.12` tags to the latest release commit. ```bash git fetch --all git checkout origin v1.12.x git tag -d v1.12 git tag v1.12 git push origin v1.12 -f git tag -d v1 git tag v1 git push origin v1 -f ``` - [ ] Submit Scala CLI MSI installer `scala-cli-x86_64-pc-win32.msi` for malware analysis. The Msi file must be uploaded using this [service](https://www.microsoft.com/en-us/wdsi/filesubmission). For more information on this process, refer [here](windows-antimalware-analysis.md). - [ ] Unmark release as `pre-release`. - [ ] Announce the new release - [ ] announce on Twitter - [ ] announce on Discord - [ ] announce on Reddit if the release contains any noteworthy changes - [ ] Create a ticket for the next release using the `Plan a release` template and assign it to the person responsible. ================================================ FILE: .github/release/windows-antimalware-analysis.md ================================================ # Microsoft anti-malware analysis As new Scala CLI are (wrongly) assumed to be PUA (potentially unwanted applications) by Microsoft Defender SmartScreen on Windows, we need to submit them for analysis to Microsoft after release. Note: the analysis may take time, and the results may not be immediately available. Sometimes it's days, sometimes it's weeks. It may even occur that the new release gets analysed while the previous one is still in the pipeline due to reasons unknown to us. As those eventually do pull through, we can't do much about it. ## Submitting a file for analysis After going through the [release procedure](release-procedure.md), we need to submit the MSI installer and the EXE launcher for analysis: - [ ] Download the `scala-cli-x86_64-pc-win32.msi` and upload it using [this service](https://www.microsoft.com/en-us/wdsi/filesubmission). - [ ] Download the `scala-cli-x86_64-pc-win32.zip`, extract it and upload `scala-cli.exe` using [the same service](https://www.microsoft.com/en-us/wdsi/filesubmission). You will need to log in using your company account authorised by the VirtusLab IT division. If you don't have one or if the one you do have doesn't have the right permissions (even though you are a maintainer of the Scala CLI repository), be sure to reach out to IT. ## Submission form When reaching https://www.microsoft.com/en-us/wdsi/filesubmission, you will be presented with a form to fill out. ![image](img/submit-for-malware-analysis-1.png) Submit file as a `Software Developer` and click continue. ![image](img/submit-for-malware-analysis-2.png) Make sure to grant your team members access to the submission by adding their emails in the `Give additional user s access to the submission` section. You can find the current Scala CLI team listed in the [Scala CLI publish module definition](../../project/publish.sc) You might also want to add the `scala-cli@virtuslab.com` group email address. Select `Windows Server Antimalware` as the security product used to scan the file. Fill in `VirtusLab` as the `Company Name`. ![image](img/submit-for-malware-analysis-3.png) #### What do you believe this file is? Select `Incorrectly detected as PUA (potentially unwanted application)`. #### Detection name Microsoft Defender SmartScreen prevented an unrecognised app from starting. #### Definition version The version number for the Scala CLI release. #### Additional information When uploading the installer (`*.msi`), paste the following, fixing swapping out the version number and release link accordingly. ```text This is the Scala CLI v installer for Microsoft Windows. Scala CLI is the official runner of the Scala programming language. For more information check https://github.com/VirtusLab/scala-cli/releases/tag/v ``` For the launcher (`*.exe`), use the (almost identical) following text: ```text This is the Scala CLI v launcher for Microsoft Windows. Scala CLI is the official runner of the Scala programming language. For more information check https://github.com/VirtusLab/scala-cli/releases/tag/v ``` Click continue. ![image](img/submit-for-malware-analysis-4.png) You might have to verify that you're a human, after which the submission should proceed. ![image](img/submit-for-malware-analysis-5.png) Double-check the submission details are correct, ending the process. ================================================ FILE: .github/scripts/build-website.sh ================================================ #!/usr/bin/env bash set -e yarn --cwd website install yarn --cwd website build ================================================ FILE: .github/scripts/check-cross-version-deps.sc ================================================ #!/usr/bin/env -S scala-cli shebang //> using scala 3 //> using toolkit default //> using options -Werror -Wunused:all val modules = os.proc(os.pwd / "mill", "-i", "resolve", "__[]") .call(cwd = os.pwd) .out .lines() for { module <- modules } { println(s"Checking for $module...") val depRegex = "[│└├─\\S\\s]+\\s([\\w.-]+):([\\w.-]+):([\\w\\s\\S.-]+)".r val scalaDepSuffixRegex = "^(.+?)(_[23](?:\\.\\d{2})?)?$".r val deps = os.proc(os.pwd / "mill", "-i", s"$module.showMvnDepsTree") .call(cwd = os.pwd) .out .lines() .filter(_.count(_ == ':') == 2) .map { case depRegex(org, name, depVersion) => (org, name, depVersion) } val invalidOrgAndName = "invalid:invalid" val scalaVersionsByOrgAndName = deps .groupBy { case (org, scalaDepSuffixRegex(nameWithoutSuffix, _), _) => s"$org:$nameWithoutSuffix" case _ => invalidOrgAndName } .collect { case (key, entries) if key != invalidOrgAndName => key -> entries .collect { case (_, scalaDepSuffixRegex(_, scalaVersion), _) => scalaVersion } .distinct } .filter { case (_, scalaVersions) => scalaVersions.head != null } // filter out non-Scala deps println("Checking for clashing dependency Scala versions...") val conflictEntries: Map[String, Vector[String]] = scalaVersionsByOrgAndName .filter { case (key, scalaVersions) => if scalaVersions.length == 1 then println(s"[info] $key${scalaVersions.head} (OK)") false else println( s"[${Console.RED}error${Console.RESET}] $key: multiple conflicting Scala versions: ${scalaVersions.mkString(", ")}" ) true } if conflictEntries.nonEmpty then println(s"${Console.RED}ERROR: Found ${conflictEntries.size} conflicting entries for $module:") conflictEntries.foreach { case (key, scalaVersions) => println(s" $key: multiple conflicting Scala versions: ${scalaVersions.mkString(", ")}") } println(Console.RESET) sys.exit(1) else println(s"[info] $module OK") } println("Checks completed for:") modules.foreach(m => println(s" $m")) println("No conflicts detected.") sys.exit(0) ================================================ FILE: .github/scripts/check-override-keywords.sh ================================================ #!/usr/bin/env bash set -euo pipefail # Checks the PR body for [test_*] override keywords. # Inputs (env vars): EVENT_NAME, PR_BODY # Outputs: writes override=true/false pairs to $GITHUB_OUTPUT and a summary table to $GITHUB_STEP_SUMMARY if [[ "$EVENT_NAME" != "pull_request" ]]; then echo "Non-PR event, setting all overrides to true" for override in test_all test_native test_integration test_docs test_format; do echo "$override=true" >> "$GITHUB_OUTPUT" done exit 0 fi TEST_ALL=false; TEST_NATIVE=false; TEST_INTEGRATION=false; TEST_DOCS=false; TEST_FORMAT=false check_override() { local keyword="$1" local var_name="$2" if printf '%s' "$PR_BODY" | grep -qF "$keyword"; then eval "$var_name=true" echo "Override $keyword found" fi } check_override "[test_all]" "TEST_ALL" check_override "[test_native]" "TEST_NATIVE" check_override "[test_integration]" "TEST_INTEGRATION" check_override "[test_docs]" "TEST_DOCS" check_override "[test_format]" "TEST_FORMAT" echo "Override keywords:" echo " test_all=$TEST_ALL" echo " test_native=$TEST_NATIVE" echo " test_integration=$TEST_INTEGRATION" echo " test_docs=$TEST_DOCS" echo " test_format=$TEST_FORMAT" echo "test_all=$TEST_ALL" >> "$GITHUB_OUTPUT" echo "test_native=$TEST_NATIVE" >> "$GITHUB_OUTPUT" echo "test_integration=$TEST_INTEGRATION" >> "$GITHUB_OUTPUT" echo "test_docs=$TEST_DOCS" >> "$GITHUB_OUTPUT" echo "test_format=$TEST_FORMAT" >> "$GITHUB_OUTPUT" echo "## Override keywords" >> "$GITHUB_STEP_SUMMARY" echo "| Keyword | Active |" >> "$GITHUB_STEP_SUMMARY" echo "|---------|--------|" >> "$GITHUB_STEP_SUMMARY" echo "| [test_all] | $TEST_ALL |" >> "$GITHUB_STEP_SUMMARY" echo "| [test_native] | $TEST_NATIVE |" >> "$GITHUB_STEP_SUMMARY" echo "| [test_integration] | $TEST_INTEGRATION |" >> "$GITHUB_STEP_SUMMARY" echo "| [test_docs] | $TEST_DOCS |" >> "$GITHUB_STEP_SUMMARY" echo "| [test_format] | $TEST_FORMAT |" >> "$GITHUB_STEP_SUMMARY" ================================================ FILE: .github/scripts/choco/scala-cli.nuspec ================================================ scala-cli @LAUNCHER_VERSION@ Scala CLI virtuslab-chocolatey virtuslab-chocolatey scala-cli Scala CLI Scala CLI is a command-line tool to interact with the Scala language. It lets you compile, run, test, and package your Scala code (and more!) https://github.com/VirtusLab/scala-cli/tree/main/.github/scripts/choco https://github.com/VirtusLab/scala-cli https://scala-cli.virtuslab.org/ https://github.com/VirtusLab/scala-cli/issues © 2021-2022 VirtusLab Sp. z. o. o. https://cdn.jsdelivr.net/gh/VirtusLab/scala-cli@e4c0eb72276ae77e689c61a83230ec16324791e8/.github/scripts/choco/logo.ico https://github.com/VirtusLab/scala-cli/blob/main/LICENSE true https://github.com/VirtusLab/scala-cli/releases ================================================ FILE: .github/scripts/choco/tools/chocolateyinstall.ps1 ================================================ $ErrorActionPreference = 'Stop'; $url64 = '@LAUNCHER_URL@' $packageArgs = @{ packageName = 'scala-cli' fileType = 'MSI' url64bit = $url64 softwareName = 'Scala CLI' checksum64 = '@LAUNCHER_SHA256@' checksumType64= 'sha256' silentArgs = "/qn /norestart" validExitCodes= @(0) } Install-ChocolateyPackage @packageArgs ================================================ FILE: .github/scripts/classify-changes.sh ================================================ #!/usr/bin/env bash set -euo pipefail # Classifies changed files into categories for CI job filtering. # Inputs (env vars): EVENT_NAME, BASE_REF # Outputs: writes category=true/false pairs to $GITHUB_OUTPUT and a summary table to $GITHUB_STEP_SUMMARY if [[ "$EVENT_NAME" != "pull_request" ]]; then echo "Non-PR event ($EVENT_NAME), setting all categories to true" for cat in code docs ci format_config benchmark gifs mill_wrapper; do echo "$cat=true" >> "$GITHUB_OUTPUT" done exit 0 fi CHANGED_FILES=$(git diff --name-only "origin/$BASE_REF...HEAD" || echo "DIFF_FAILED") if [[ "$CHANGED_FILES" == "DIFF_FAILED" ]]; then echo "::warning::Failed to compute diff, running all jobs" for cat in code docs ci format_config benchmark gifs mill_wrapper; do echo "$cat=true" >> "$GITHUB_OUTPUT" done exit 0 fi CODE=false; DOCS=false; CI=false; FORMAT_CONFIG=false; BENCHMARK=false; GIFS=false; MILL_WRAPPER=false while IFS= read -r file; do case "$file" in modules/*|build.mill|project/*) CODE=true ;; website/*) DOCS=true ;; .github/*) CI=true ;; .scalafmt.conf|.scalafix.conf) FORMAT_CONFIG=true ;; gcbenchmark/*) BENCHMARK=true ;; gifs/*) GIFS=true ;; mill|mill.bat) MILL_WRAPPER=true ;; esac done <<< "$CHANGED_FILES" echo "Change categories:" echo " code=$CODE" echo " docs=$DOCS" echo " ci=$CI" echo " format_config=$FORMAT_CONFIG" echo " benchmark=$BENCHMARK" echo " gifs=$GIFS" echo " mill_wrapper=$MILL_WRAPPER" echo "code=$CODE" >> "$GITHUB_OUTPUT" echo "docs=$DOCS" >> "$GITHUB_OUTPUT" echo "ci=$CI" >> "$GITHUB_OUTPUT" echo "format_config=$FORMAT_CONFIG" >> "$GITHUB_OUTPUT" echo "benchmark=$BENCHMARK" >> "$GITHUB_OUTPUT" echo "gifs=$GIFS" >> "$GITHUB_OUTPUT" echo "mill_wrapper=$MILL_WRAPPER" >> "$GITHUB_OUTPUT" echo "## Change categories" >> "$GITHUB_STEP_SUMMARY" echo "| Category | Changed |" >> "$GITHUB_STEP_SUMMARY" echo "|----------|---------|" >> "$GITHUB_STEP_SUMMARY" for cat in code docs ci format_config benchmark gifs mill_wrapper; do val=$(eval echo \$$( echo $cat | tr 'a-z' 'A-Z')) echo "| $cat | $val |" >> "$GITHUB_STEP_SUMMARY" done ================================================ FILE: .github/scripts/docker/ScalaCliDockerFile ================================================ FROM debian:stable-slim RUN apt update && apt install build-essential libz-dev clang procps -y ADD scala-cli /usr/bin/ RUN \ echo "println(1)" | scala-cli -S 3 - -v -v -v && \ echo "println(1)" | scala-cli -S 2.13 - -v -v -v && \ echo "println(1)" | scala-cli -S 2.12 - -v -v -v RUN \ echo "println(1)" | scala-cli --power package --native _.sc --force && \ echo "println(1)" | scala-cli --power package --native-image _.sc --force ENTRYPOINT ["scala-cli"] ================================================ FILE: .github/scripts/docker/ScalaCliSlimDockerFile ================================================ FROM debian:stable-slim AS build-env FROM gcr.io/distroless/base-debian12 ADD scala-cli /usr/local/bin/scala-cli COPY --from=build-env /lib/x86_64-linux-gnu/libz.so.1 /lib/x86_64-linux-gnu/libz.so.1 ENTRYPOINT ["/usr/local/bin/scala-cli"] ================================================ FILE: .github/scripts/generate-docker-image.sh ================================================ #!/usr/bin/env bash set -eu ROOT="$(cd "$(dirname "$0")/../.." && pwd)" WORKDIR="$ROOT/out/docker-workdir" mkdir -p "$WORKDIR" ./mill -i copyTo --task 'cli[]'.nativeImageStatic --dest "$WORKDIR/scala-cli" 1>&2 cd "$WORKDIR" docker build -t scala-cli -f "$ROOT/.github/scripts/docker/ScalaCliDockerFile" . ================================================ FILE: .github/scripts/generate-junit-reports.sc ================================================ #!/usr/bin/env -S scala-cli shebang //> using scala 3 //> using toolkit default //> using dep org.scala-lang.modules::scala-xml:2.4.0 //> using options -Werror -Wunused:all // adapted from https://github.com/vic/mill-test-junit-report import java.io.File import scala.collection.mutable.ArrayBuffer import scala.annotation.tailrec import java.nio.file.Paths import scala.util.Try case class Trace(declaringClass: String, methodName: String, fileName: String, lineNumber: Int) { override def toString: String = s"$declaringClass.$methodName($fileName:$lineNumber)" } case class Failure(name: String, message: String, trace: Seq[Trace]) case class Test( fullyQualifiedName: String, selector: String, duration: Double, failure: Option[Failure] ) @tailrec def findFiles(paths: Seq[os.Path], result: Seq[os.Path] = Nil): Seq[os.Path] = paths match case Nil => result case head :: tail => val newFiles = if head.segments.contains("test") && head.last.endsWith(".dest") && os.isDir(head) then os.list(head).filter(f => f.last == "out.json").toList else Seq.empty val newDirs = os.list(head).filter(p => os.isDir(p)).toList findFiles(tail ++ newDirs, result ++ newFiles) extension (s: String) def toNormalisedPath: os.Path = if Paths.get(s).isAbsolute then os.Path(s) else os.Path(s, os.pwd) def printUsageMessage(): Unit = println("Usage: generate-junit-reports ") if args.length != 4 then { println(s"Error: provided too few arguments: ${args.length}") printUsageMessage() System.exit(1) } val id: String = args(0) val name: String = args(1) if new File(args(2)).exists() then { println(s"Error: specified output path already exists: ${args(2)}") System.exit(1) } val into = args(2).toNormalisedPath val pathArg = args(3) val rootPath: os.Path = if Paths.get(pathArg).isAbsolute then os.Path(pathArg) else os.Path(pathArg, os.pwd) if !os.isDir(rootPath) then { println(s"The path provided is not a directory: $pathArg") System.exit(1) } val reports: Seq[os.Path] = findFiles(Seq(rootPath)) println(s"Found ${reports.length} mill json reports:") println(reports.mkString("\n")) if reports.isEmpty then println("Warn: no reports found!") println("Reading reports...") val tests: Seq[Test] = reports.map(x => ujson.read(x.toNIO)).flatMap { json => json(1).value.asInstanceOf[ArrayBuffer[ujson.Obj]].map { test => Test( fullyQualifiedName = test("fullyQualifiedName").str, selector = test("selector").str, duration = test("duration").num / 1000.0, failure = test("status").str match { case "Failure" => Some(Failure( name = test("exceptionName")(0).str, message = test("exceptionMsg")(0).str, trace = test("exceptionTrace")(0).arr.map { st => val declaringClass = st("declaringClass").str val methodName = st("methodName").str val fileName = st("fileName")(0).str val lineNumber = st("lineNumber").num.toInt Trace(declaringClass, methodName, fileName, lineNumber) }.toList )) case _ => None } ) } } println(s"Found ${tests.length} tests.") if tests.isEmpty then println("Warn: no tests found!") println("Generating JUnit XML report...") val suites = tests.groupBy(_.fullyQualifiedName).map { case (suit, tests) => val testcases = tests.map { test => { test.failure.map { failure => val maybeTrace = Try(failure.trace(1)).toOption val fileName = maybeTrace.map(_.fileName).getOrElse("unknown") val lineNumber = maybeTrace.map(_.lineNumber).getOrElse(-1) ERROR: {failure.message} Category: {failure.name} File: {fileName} Line: {lineNumber} }.orNull } { test.failure.map { failure => { failure.trace.mkString(s"${failure.name}: ${failure.message}", "\n at ", "") } }.orNull } } {testcases} } val n = {suites} val prettyXmlPrinter = new scala.xml.PrettyPrinter(80, 2) val xmlToSave = scala.xml.XML.loadString(prettyXmlPrinter.format(n)) scala.xml.XML.save(filename = into.toString(), node = xmlToSave, xmlDecl = true) println(s"Generated report at: $into") ================================================ FILE: .github/scripts/generate-native-image.sh ================================================ #!/usr/bin/env bash set -e COMMAND="cli[].base-image.writeDefaultNativeImageScript" # temporary, until we pass JPMS options to native-image, # see https://www.graalvm.org/release-notes/22_2/#native-image export USE_NATIVE_IMAGE_JAVA_PLATFORM_MODULE_SYSTEM=false # Using 'mill -i' so that the Mill process doesn't outlive this invocation if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then ./mill.bat -i ci.copyJvm --dest jvm export JAVA_HOME="$(pwd -W | sed 's,/,\\,g')\\jvm" export GRAALVM_HOME="$JAVA_HOME" export PATH="$(pwd)/bin:$PATH" echo "PATH=$PATH" # this part runs into connection problems on Windows, so we retry up to 5 times MAX_RETRIES=5 RETRY_COUNT=0 while (( RETRY_COUNT < MAX_RETRIES )); do ./mill.bat -i "$COMMAND" --scriptDest generate-native-image.bat if [[ $? -ne 0 ]]; then echo "Error occurred during 'mill.bat -i $COMMAND generate-native-image.bat' command. Retrying... ($((RETRY_COUNT + 1))/$MAX_RETRIES)" (( RETRY_COUNT++ )) sleep 2 else ./generate-native-image.bat if [[ $? -ne 0 ]]; then echo "Error occurred during 'generate-native-image.bat'. Retrying... ($((RETRY_COUNT + 1))/$MAX_RETRIES)" (( RETRY_COUNT++ )) sleep 2 else echo "'generate-native-image.bat' succeeded with $RETRY_COUNT retries." break fi fi done if (( RETRY_COUNT == MAX_RETRIES )); then echo "Exceeded maximum retry attempts. Exiting with error." exit 1 fi else if [ $# == "0" ]; then if [[ "$OSTYPE" == "linux-gnu" ]]; then if [[ "$(uname -m)" == "aarch64" ]]; then COMMAND="cli[].base-image.writeDefaultNativeImageScript" CLEANUP=("true") else COMMAND="cli[].linux-docker-image.writeDefaultNativeImageScript" CLEANUP=("sudo" "rm" "-rf" "out/cli/linux-docker-image/nativeImageDockerWorkingDir") fi else CLEANUP=("true") fi else case "$1" in "static") COMMAND="cli[].static-image.writeDefaultNativeImageScript" CLEANUP=("sudo" "rm" "-rf" "out/cli/static-image/nativeImageDockerWorkingDir") ;; "mostly-static") COMMAND="cli[].mostly-static-image.writeDefaultNativeImageScript" CLEANUP=("sudo" "rm" "-rf" "out/cli/mostly-static-image/nativeImageDockerWorkingDir") ;; *) echo "Invalid image name: $1" 1>&2 exit 1 ;; esac fi ./mill -i "$COMMAND" --scriptDest generate-native-image.sh bash ./generate-native-image.sh "${CLEANUP[@]}" fi ================================================ FILE: .github/scripts/generate-os-packages.sh ================================================ #!/usr/bin/env bash set -eu ARCHITECTURE="x86_64" # Set the default architecture if [[ "$OSTYPE" == "linux-gnu"* ]] || [[ "$OSTYPE" == "darwin"* ]]; then # When running on macOS and Linux lets determine the architecture ARCHITECTURE=$(uname -m) fi if [[ $# -eq 1 ]]; then # architecture gets overridden by command line param ARCHITECTURE=$1 fi ARTIFACTS_DIR="artifacts/" mkdir -p "$ARTIFACTS_DIR" if [[ -z "$OSTYPE" ]]; then mill="./mill.bat" else mill="./mill" fi packager() { "$mill" -i packager.run "$@" } launcher() { local launcherMillCommand="cli[].nativeImage" local launcherName if [[ "${OS-}" == "Windows_NT" ]]; then launcherName="scala.exe" else launcherName="scala" fi "$mill" -i copyTo --task "$launcherMillCommand" --dest "$launcherName" 1>&2 echo "$launcherName" } version() { "$mill" -i writePackageVersionTo --dest scala-cli-version 1>&2 cat scala-cli-version } shortVersion() { "$mill" -i writeShortPackageVersionTo --dest scala-cli-short-version 1>&2 cat scala-cli-short-version } generate_deb() { packager \ --deb \ --version "$(version)" \ --source-app-path "$(launcher)" \ --output "$ARTIFACTS_DIR/scala-cli.deb" \ --description "Scala CLI" \ --maintainer "scala-cli@virtuslab.com" \ --launcher-app "scala-cli" \ --priority "optional" \ --section "devel" mv "$ARTIFACTS_DIR/scala-cli.deb" "$ARTIFACTS_DIR/scala-cli-x86_64-pc-linux.deb" } generate_rpm() { packager \ --rpm \ --version "$(shortVersion)" \ --source-app-path "$(launcher)" \ --output "$ARTIFACTS_DIR/scala-cli-x86_64-pc-linux.rpm" \ --description "Scala CLI" \ --maintainer "scala-cli@virtuslab.com" \ --license "ASL 2.0" \ --launcher-app "scala-cli" } generate_pkg() { arch=$1 packager \ --pkg \ --version "$(version)" \ --source-app-path "$(launcher)" \ --output "$ARTIFACTS_DIR/scala-cli-$arch-apple-darwin.pkg" \ --identifier "scala-cli" \ --launcher-app "scala-cli" } generate_msi() { # Having the MSI automatically install Visual C++ redistributable when needed, # see https://wixtoolset.org/documentation/manual/v3/howtos/redistributables_and_install_checks/install_vcredist.html "$mill" -i ci.writeWixConfigExtra --dest wix-visual-cpp-redist.xml packager \ --msi \ --version "$(shortVersion)" \ --source-app-path "$(launcher)" \ --output "$ARTIFACTS_DIR/scala-cli-x86_64-pc-win32.msi" \ --product-name "Scala CLI" \ --maintainer "scala-cli@virtuslab.com" \ --launcher-app "scala-cli" \ --license-path "./LICENSE" \ --exit-dialog "To run Scala CLI, open a Command window, and type scala-cli + Enter. If scala-cli cannot be found, ensure that the Command window was opened after Scala CLI was installed." \ --logo-path "./logo.png" \ --suppress-validation \ --extra-configs wix-visual-cpp-redist.xml \ --wix-upgrade-code-guid "C74FC9A1-9381-40A6-882F-9044C603ABD9" rm -f "$ARTIFACTS_DIR/"*.wixpdb || true } generate_sdk() { local sdkDirectory local binName if [[ "$OSTYPE" == "linux-gnu"* ]]; then if [[ "$ARCHITECTURE" == "aarch64" ]] || [[ "$ARCHITECTURE" == "x86_64" ]]; then sdkDirectory="scala-cli-$ARCHITECTURE-pc-linux-static-sdk" else echo "scala-cli is not supported on $ARCHITECTURE" exit 2 fi binName="scala-cli" elif [[ "$OSTYPE" == "darwin"* ]]; then if [[ "$ARCHITECTURE" == "arm64" ]]; then sdkDirectory="scala-cli-aarch64-apple-darwin-sdk" else sdkDirectory="scala-cli-x86_64-apple-darwin-sdk" fi binName="scala-cli" elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then sdkDirectory="scala-cli-x86_64-pc-win32-sdk" binName="scala-cli.exe" else echo "Unrecognized operating system: $OSTYPE" 1>&2 exit 1 fi mkdir -p "$sdkDirectory"/bin cp "$(launcher)" "$sdkDirectory"/bin/"$binName" if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then 7z a "$sdkDirectory".zip "$sdkDirectory" else zip -r "$sdkDirectory".zip "$sdkDirectory" fi mv "$sdkDirectory".zip "$ARTIFACTS_DIR/"/"$sdkDirectory".zip } if [[ "$OSTYPE" == "linux-gnu"* ]]; then if [ "$ARCHITECTURE" == "x86_64" ]; then generate_deb generate_rpm fi generate_sdk elif [[ "$OSTYPE" == "darwin"* ]]; then if [ "$ARCHITECTURE" == "arm64" ]; then generate_pkg "aarch64" else generate_pkg "x86_64" fi generate_sdk elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then generate_msi generate_sdk else echo "Unrecognized operating system: $OSTYPE" 1>&2 exit 1 fi ================================================ FILE: .github/scripts/generate-slim-docker-image.sh ================================================ #!/usr/bin/env bash set -eu ROOT="$(cd "$(dirname "$0")/../.." && pwd)" WORKDIR="$ROOT/out/docker-slim-workdir" mkdir -p "$WORKDIR" ./mill -i copyTo --task 'cli[]'.nativeImageMostlyStatic --dest "$WORKDIR/scala-cli" 1>&2 cd "$WORKDIR" docker build -t scala-cli-slim -f "$ROOT/.github/scripts/docker/ScalaCliSlimDockerFile" . ================================================ FILE: .github/scripts/get-latest-cs.sh ================================================ #!/usr/bin/env bash set -e CS_VERSION="2.1.25-M24" DIR="$(cs get --archive "https://github.com/coursier/coursier/releases/download/v$CS_VERSION/cs-x86_64-pc-win32.zip")" cp "$DIR/"*.exe cs.exe ================================================ FILE: .github/scripts/gpg-setup.sh ================================================ #!/usr/bin/env sh # from https://github.com/coursier/apps/blob/f1d2bf568bf466a98569a85c3f23c5f3a8eb5360/.github/scripts/gpg-setup.sh echo "$PGP_SECRET" | base64 --decode | gpg --import --no-tty --batch --yes echo "allow-loopback-pinentry" >>~/.gnupg/gpg-agent.conf echo "pinentry-mode loopback" >>~/.gnupg/gpg.conf gpg-connect-agent reloadagent /bye ================================================ FILE: .github/scripts/process_release_notes.sc ================================================ #!/usr/bin/env -S scala-cli shebang //> using scala 3 //> using toolkit default //> using options -Werror -Wunused:all case class Replacement(find: String, replace: String) // Hardcoded regex replacements for processing release notes // These transform GitHub-idiomatic syntax to website-compatible Markdown val replacements: Seq[Replacement] = Seq( // 1. Contributor link: Transform "by @user in" to "by [@user](https://github.com/user) in" // Excludes bots: dependabot, github-actions, scala-steward // Only matches if not already a link (negative lookahead to avoid matching already-formatted entries) // Must come BEFORE PR link pattern to avoid interference // The pattern stops at the first " in" and doesn't match if there's a formatted link before it Replacement( find = "by @(?!dependabot\\[bot\\]|github-actions\\[bot\\]|scala-steward)([^\\[\\]]+?) in(?!.*\\[@.*?\\]\\()", replace = "by [@$1](https://github.com/$1) in" ), // 2. New contributor link: Transform "@user made" to "[@user](https://github.com/user) made" // Excludes bots: dependabot, github-actions, scala-steward // Only matches if not already a link (negative lookbehind and lookahead to avoid matching already-formatted entries) Replacement( find = "(? replacements.foldLeft(line) { (current, replacement) => try val regex = replacement.find.r // Manually handle replacement to avoid issues with $ in replacement strings var result = current val matches = regex.findAllMatchIn(current).toSeq.reverse // Process from end to avoid offset issues for matchData <- matches do // Build replacement string by substituting $1, $2, etc. with actual group values var replacementText = replacement.replace // Replace $1, $2, etc. with actual group values (in reverse order to avoid replacing $11 when we mean $1) for i <- matchData.groupCount to 1 by -1 do val groupValue = if matchData.group(i) != null then matchData.group(i) else "" replacementText = replacementText.replace(s"$$$i", groupValue) // Replace the matched portion result = result.substring(0, matchData.start) + replacementText + result.substring(matchData.end) result catch case e: Exception => System.err.println( s"Warning: Failed to apply regex '${replacement.find}': ${e.getMessage}" ) if System.getenv("DEBUG") == "true" then e.printStackTrace() current } } // Rejoin lines with newlines, preserving original line endings val lineEnding = if text.contains("\r\n") then "\r\n" else if text.contains("\n") then "\n" else System.lineSeparator() processedLines.mkString(lineEnding) + (if text.endsWith("\n") || text.endsWith("\r\n") then lineEnding else "") } def printUsageMessage(): Unit = { println("Usage: process_release_notes.sc ") println("Commands:") println(" apply - Apply regexes to the file (modifies in place)") println(" check - Check if file needs regexes applied (exits with error if needed)") println(" verify - Verify file has regexes applied (exits with error if not)") } if args.length < 2 then println(s"Error: too few arguments: ${args.length}") printUsageMessage() sys.exit(1) val command = args(0) val filePath = os.Path(args(1), os.pwd) if !os.exists(filePath) then println(s"Error: file does not exist: $filePath") sys.exit(1) if System.getenv("DEBUG") == "true" then println(s"Loaded ${replacements.length} replacement patterns") replacements.zipWithIndex.foreach { case (r, i) => println(s" Pattern ${i + 1}: Find='${r.find}', Replace='${r.replace}'") } val originalContent = os.read(filePath) val transformedContent = applyReplacements(originalContent, replacements) command match case "apply" => os.write.over(filePath, transformedContent) println(s"Applied regexes to: $filePath") case "check" => if originalContent != transformedContent then println(s"Error: File $filePath needs regexes applied") println("Run: .github/scripts/process_release_notes.sc apply ") sys.exit(1) else println(s"File $filePath is already processed correctly") sys.exit(0) case "verify" => // Check for patterns that should have been transformed // All patterns have negative lookaheads to avoid matching already-formatted entries val patternsToCheck = replacements val needsTransformation = patternsToCheck.exists { replacement => try val regex = replacement.find.r regex.findFirstIn(originalContent).isDefined catch case _: Exception => false } if needsTransformation then println(s"Error: File $filePath contains patterns that need transformation") println("The following patterns were found that should be transformed:") patternsToCheck.foreach { replacement => try val regex = replacement.find.r if regex.findFirstIn(originalContent).isDefined then println(s" - Pattern: ${replacement.find}") catch case _: Exception => () } println("Run: .github/scripts/process_release_notes.sc apply ") sys.exit(1) else println(s"File $filePath is properly formatted") sys.exit(0) case _ => println(s"Error: unknown command: $command") printUsageMessage() sys.exit(1) ================================================ FILE: .github/scripts/publish-docker-images.sh ================================================ #!/usr/bin/env bash set -eu RAW_VERSION="$(./mill -i ci.publishVersion)" SCALA_CLI_VERSION="${RAW_VERSION##* }" docker tag scala-cli virtuslab/scala-cli:latest docker tag scala-cli virtuslab/scala-cli:"$SCALA_CLI_VERSION" docker push virtuslab/scala-cli:latest docker push virtuslab/scala-cli:"$SCALA_CLI_VERSION" ================================================ FILE: .github/scripts/publish-sdkman.sh ================================================ #!/usr/bin/env bash # from https://github.com/lampepfl/dotty/blob/37e997abc2bf4d42321492acaf7f7832ee7ce146/.github/workflows/scripts/publish-sdkman.sh # This is script for publishing Scala CLI on SDKMAN. # It's releasing and announcing the release of Scala CLI on SDKMAN. # # Requirement: # - the latest stable version of Scala CLI should be available in github artifacts set -eu version() { "./mill" -i writePackageVersionTo --dest scala-cli-version 1>&2 cat scala-cli-version } SCALA_CLI_VERSION="$(version)" ARCHS=("x86_64" "aarch64" "x86_64" "aarch64" "x86_64") UNAMES=("pc-linux-static-sdk" "pc-linux-static-sdk" "apple-darwin-sdk" "apple-darwin-sdk" "pc-win32-sdk") PLATFORMS=("LINUX_64" "LINUX_ARM64" "MAC_OSX" "MAC_ARM64" "WINDOWS_64") for i in "${!PLATFORMS[@]}"; do SCALA_CLI_URL="https://github.com/VirtusLab/scala-cli/releases/download/v$SCALA_CLI_VERSION/scala-cli-${ARCHS[i]}-${UNAMES[i]}.zip" # Release a new Candidate Version curl --silent --show-error --fail \ -X POST \ -H "Consumer-Key: $SDKMAN_KEY" \ -H "Consumer-Token: $SDKMAN_TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{"candidate": "scalacli", "version": "'"$SCALA_CLI_VERSION"'", "url": "'"$SCALA_CLI_URL"'", "platform": "'"${PLATFORMS[i]}"'" }' \ https://vendors.sdkman.io/release if [[ $? -ne 0 ]]; then echo "Fail sending POST request to releasing Scala CLI on SDKMAN on platform: ${PLATFORMS[i]}." exit 1 fi done # Set SCALA_CLI_VERSION as Default for Candidate curl --silent --show-error --fail \ -X PUT \ -H "Consumer-Key: $SDKMAN_KEY" \ -H "Consumer-Token: $SDKMAN_TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{"candidate": "scalacli", "version": "'"$SCALA_CLI_VERSION"'" }' \ https://vendors.sdkman.io/default if [[ $? -ne 0 ]]; then echo "Fail sending PUT request to announcing the release of Scala CLI on SDKMAN." exit 1 fi ================================================ FILE: .github/scripts/publish-slim-docker-images.sh ================================================ #!/usr/bin/env bash set -eu RAW_VERSION="$(./mill -i ci.publishVersion)" SCALA_CLI_VERSION="${RAW_VERSION##* }" docker tag scala-cli-slim virtuslab/scala-cli-slim:latest docker tag scala-cli-slim virtuslab/scala-cli-slim:"$SCALA_CLI_VERSION" docker push virtuslab/scala-cli-slim:latest docker push virtuslab/scala-cli-slim:"$SCALA_CLI_VERSION" ================================================ FILE: .github/scripts/scala-cli.rb.template ================================================ # typed: false # frozen_string_literal: true # ScalaCli Formula class ScalaCli < Formula desc "Launcher for ScalaCli" homepage "https://virtuslab.github.io/scala-cli/" url (RUBY_PLATFORM.include? "arm64") ? "@ARM64_LAUNCHER_URL@" : "@X86_LAUNCHER_URL@" version "@LAUNCHER_VERSION@" sha256 (RUBY_PLATFORM.include? "arm64") ? "@ARM64_LAUNCHER_SHA256@" : "@X86_LAUNCHER_SHA256@" license "Apache-2.0" def install if (RUBY_PLATFORM.include? "arm64") bin.install "scala-cli-aarch64-apple-darwin" => "scala-cli" else bin.install "scala-cli-x86_64-apple-darwin" => "scala-cli" end end test do (testpath / "Hello.scala").write "object Hello { def main(args: Array[String]): Unit = println(\"Hello from Scala\") }" output = shell_output("#{bin}/scala-cli Hello.scala") assert_equal ["Hello from Scala\n"], output.lines end end ================================================ FILE: .github/scripts/scala.rb.template ================================================ # typed: false # frozen_string_literal: true # Experimental Scala Formula class Scala < Formula desc "Experimental launcher for Scala" homepage "https://virtuslab.github.io/scala-cli/" url (RUBY_PLATFORM.include? "arm64") ? "@ARM64_LAUNCHER_URL@" : "@X86_LAUNCHER_URL@" version "@LAUNCHER_VERSION@" sha256 (RUBY_PLATFORM.include? "arm64") ? "@ARM64_LAUNCHER_SHA256@" : "@X86_LAUNCHER_SHA256@" license "Apache-2.0" def install if (RUBY_PLATFORM.include? "arm64") bin.install "scala-cli-aarch64-apple-darwin" => "scala-cli" else bin.install "scala-cli-x86_64-apple-darwin" => "scala-cli" end bin.install_symlink "scala-cli" => "scala" end test do (testpath / "Hello.scala").write "object Hello { def main(args: Array[String]): Unit = println(\"Hello from Scala\") }" output = shell_output("#{bin}/scala-cli Hello.scala") assert_equal ["Hello from Scala\n"], output.lines end end ================================================ FILE: .github/scripts/update-website.sh ================================================ #!/usr/bin/env bash set -e git config --global user.name "gh-actions" git config --global user.email "actions@github.com" cd website yarn install yarn build yarn deploy ================================================ FILE: .github/scripts/verify_old_cpus.sh ================================================ #!/usr/bin/env bash set -e # Verifies that the native launcher runs on older x86_64 CPUs (without AVX/AVX2/FMA). # Uses QEMU user-mode emulation with a Westmere CPU model (2010, SSE4.2 but no AVX). LAUNCHER_GZ="${1:?Usage: $0 }" sudo apt-get update -qq && sudo apt-get install -y -qq qemu-user > /dev/null LAUNCHER="/tmp/scala-cli-compat-test" gunzip -c "$LAUNCHER_GZ" > "$LAUNCHER" chmod +x "$LAUNCHER" echo "Running native launcher under QEMU with Westmere CPU (no AVX/AVX2/FMA)..." qemu-x86_64 -cpu Westmere "$LAUNCHER" version echo "CPU compatibility check passed." ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: - main tags: - "v*" pull_request: workflow_dispatch: concurrency: group: ${{ github.ref }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} jobs: changes: runs-on: ubuntu-24.04 timeout-minutes: 5 outputs: code: ${{ steps.classify.outputs.code }} docs: ${{ steps.classify.outputs.docs }} ci: ${{ steps.classify.outputs.ci }} format_config: ${{ steps.classify.outputs.format_config }} benchmark: ${{ steps.classify.outputs.benchmark }} gifs: ${{ steps.classify.outputs.gifs }} mill_wrapper: ${{ steps.classify.outputs.mill_wrapper }} test_all: ${{ steps.overrides.outputs.test_all }} test_native: ${{ steps.overrides.outputs.test_native }} test_integration: ${{ steps.overrides.outputs.test_integration }} test_docs: ${{ steps.overrides.outputs.test_docs }} test_format: ${{ steps.overrides.outputs.test_format }} steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Classify changes id: classify env: EVENT_NAME: ${{ github.event_name }} BASE_REF: ${{ github.event.pull_request.base.ref }} run: .github/scripts/classify-changes.sh - name: Check override keywords id: overrides env: EVENT_NAME: ${{ github.event_name }} PR_BODY: ${{ github.event.pull_request.body }} run: .github/scripts/check-override-keywords.sh unit-tests: needs: [changes] timeout-minutes: 120 runs-on: ubuntu-24.04 env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.mill_wrapper == 'true' || needs.changes.outputs.test_all == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping unit tests -- changes do not affect compiled code, CI, or mill wrapper." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "temurin:17" - name: Copy launcher run: ./mill -i copyJvmLauncher --directory artifacts/ if: runner.os == 'Linux' && env.SHOULD_RUN == 'true' - name: Copy bootstrapped launcher run: ./mill -i copyJvmBootstrappedLauncher --directory artifacts/ if: runner.os == 'Linux' && env.SHOULD_RUN == 'true' - uses: actions/upload-artifact@v7 if: runner.os == 'Linux' && env.SHOULD_RUN == 'true' with: name: jvm-launchers path: artifacts/ if-no-files-found: error retention-days: 2 - name: Cross compile everything if: env.SHOULD_RUN == 'true' run: ./mill -i '__[_].compile' - name: Build macros negative compilation tests if: env.SHOULD_RUN == 'true' run: ./mill -i build-macros[_].test.testNegativeCompilation - name: Unit tests if: env.SHOULD_RUN == 'true' run: ./mill -i unitTests - name: Convert Mill test reports to JUnit XML format if: (success() || failure()) && env.SHOULD_RUN == 'true' run: .github/scripts/generate-junit-reports.sc unit-tests 'Scala CLI Unit Tests' test-report.xml out/ - name: Upload test report uses: actions/upload-artifact@v7 if: (success() || failure()) && env.SHOULD_RUN == 'true' with: name: test-results-unit-tests path: test-report.xml test-fish-shell: needs: [changes] timeout-minutes: 120 runs-on: ubuntu-24.04 env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.mill_wrapper == 'true' || needs.changes.outputs.test_all == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping fish shell test -- changes do not affect compiled code, CI, or mill wrapper." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "temurin:17" - name: Install fish if: env.SHOULD_RUN == 'true' run: | sudo apt-add-repository ppa:fish-shell/release-3 sudo apt update sudo apt install fish - name: Test mill script in fish shell if: env.SHOULD_RUN == 'true' run: | fish -c './mill __.compile' jvm-bootstrapped-tests-default: needs: [changes] timeout-minutes: 150 runs-on: ubuntu-24.04 env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.test_all == 'true' || needs.changes.outputs.test_integration == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping JVM bootstrapped integration tests -- changes do not affect compiled code or CI." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "temurin:17" - name: JVM integration tests if: env.SHOULD_RUN == 'true' run: ./mill -i integration.test.jvmBootstrapped env: SCALA_CLI_IT_GROUP: 1 - name: Convert Mill test reports to JUnit XML format if: (success() || failure()) && env.SHOULD_RUN == 'true' run: .github/scripts/generate-junit-reports.sc jvm-bootstrapped-tests-default 'Scala CLI JVM Bootstrapped Tests' test-report.xml out/ - name: Upload test report uses: actions/upload-artifact@v7 if: (success() || failure()) && env.SHOULD_RUN == 'true' with: name: test-results-jvm-bootstrapped-tests-default path: test-report.xml jvm-tests-default: needs: [changes] timeout-minutes: 150 runs-on: ubuntu-24.04 env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.test_all == 'true' || needs.changes.outputs.test_integration == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping JVM integration tests (default) -- changes do not affect compiled code or CI." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "temurin:17" - name: JVM integration tests if: env.SHOULD_RUN == 'true' run: ./mill -i integration.test.jvm env: SCALA_CLI_IT_GROUP: 1 - name: Convert Mill test reports to JUnit XML format if: (success() || failure()) && env.SHOULD_RUN == 'true' run: .github/scripts/generate-junit-reports.sc jvm-tests-default 'Scala CLI JVM Tests (default)' test-report.xml out/ - name: Upload test report uses: actions/upload-artifact@v7 if: (success() || failure()) && env.SHOULD_RUN == 'true' with: name: test-results-jvm-tests-default path: test-report.xml jvm-tests-scala-2-13: needs: [changes] timeout-minutes: 150 runs-on: ubuntu-24.04 env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.test_all == 'true' || needs.changes.outputs.test_integration == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping JVM integration tests (Scala 2.13) -- changes do not affect compiled code or CI." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "temurin:17" - name: JVM integration tests if: env.SHOULD_RUN == 'true' run: ./mill -i integration.test.jvm env: SCALA_CLI_IT_GROUP: 2 - name: Convert Mill test reports to JUnit XML format if: (success() || failure()) && env.SHOULD_RUN == 'true' run: .github/scripts/generate-junit-reports.sc jvm-tests-scala-2-13 'Scala CLI JVM Tests (Scala 2.13)' test-report.xml out/ - name: Upload test report uses: actions/upload-artifact@v7 if: (success() || failure()) && env.SHOULD_RUN == 'true' with: name: test-results-jvm-tests-scala-2-13 path: test-report.xml jvm-tests-scala-2-12: needs: [changes] timeout-minutes: 150 runs-on: ubuntu-24.04 env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.test_all == 'true' || needs.changes.outputs.test_integration == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping JVM integration tests (Scala 2.12) -- changes do not affect compiled code or CI." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "temurin:17" - name: JVM integration tests if: env.SHOULD_RUN == 'true' run: ./mill -i integration.test.jvm env: SCALA_CLI_IT_GROUP: 3 - name: Convert Mill test reports to JUnit XML format if: (success() || failure()) && env.SHOULD_RUN == 'true' run: .github/scripts/generate-junit-reports.sc jvm-tests-scala-2-12 'Scala CLI JVM Tests (Scala 2.12)' test-report.xml out/ - name: Upload test report uses: actions/upload-artifact@v7 if: (success() || failure()) && env.SHOULD_RUN == 'true' with: name: test-results-jvm-tests-scala-2-12 path: test-report.xml jvm-tests-lts: needs: [changes] timeout-minutes: 150 runs-on: ubuntu-24.04 env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.test_all == 'true' || needs.changes.outputs.test_integration == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping JVM integration tests (Scala 3 LTS) -- changes do not affect compiled code or CI." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "temurin:17" - name: JVM integration tests if: env.SHOULD_RUN == 'true' run: ./mill -i integration.test.jvm env: SCALA_CLI_IT_GROUP: 4 - name: Convert Mill test reports to JUnit XML format if: (success() || failure()) && env.SHOULD_RUN == 'true' run: .github/scripts/generate-junit-reports.sc jvm-tests-lts 'Scala CLI JVM Tests (Scala 3 LTS)' test-report.xml out/ - name: Upload test report uses: actions/upload-artifact@v7 if: (success() || failure()) && env.SHOULD_RUN == 'true' with: name: test-results-jvm-tests-lts path: test-report.xml jvm-tests-rc: needs: [changes] timeout-minutes: 150 runs-on: ubuntu-24.04 env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.test_all == 'true' || needs.changes.outputs.test_integration == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping JVM integration tests (RC) -- changes do not affect compiled code or CI." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "temurin:17" - name: JVM integration tests if: env.SHOULD_RUN == 'true' run: ./mill -i integration.test.jvm env: SCALA_CLI_IT_GROUP: 5 - name: Convert Mill test reports to JUnit XML format if: (success() || failure()) && env.SHOULD_RUN == 'true' run: .github/scripts/generate-junit-reports.sc jvm-tests-rc 'Scala CLI JVM Tests (5)' test-report.xml out/ - name: Upload test report uses: actions/upload-artifact@v7 if: (success() || failure()) && env.SHOULD_RUN == 'true' with: name: test-results-jvm-tests-rc path: test-report.xml generate-linux-launcher: needs: [changes] timeout-minutes: 120 runs-on: ubuntu-24.04 env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.test_all == 'true' || needs.changes.outputs.test_native == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping Linux native launcher generation -- changes do not affect compiled code or CI." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "temurin:17" - name: Generate native launcher if: env.SHOULD_RUN == 'true' run: .github/scripts/generate-native-image.sh - run: ./mill -i ci.setShouldPublish if: env.SHOULD_RUN == 'true' - name: Build OS packages if: env.SHOULD_PUBLISH == 'true' && env.SHOULD_RUN == 'true' run: .github/scripts/generate-os-packages.sh - name: Copy artifacts if: env.SHOULD_RUN == 'true' run: ./mill -i copyDefaultLauncher --directory artifacts/ - name: Verify native launcher CPU compatibility if: env.SHOULD_RUN == 'true' run: .github/scripts/verify_old_cpus.sh artifacts/scala-cli-x86_64-pc-linux.gz - uses: actions/upload-artifact@v7 if: env.SHOULD_RUN == 'true' with: name: linux-launchers path: artifacts/ if-no-files-found: error retention-days: 2 native-linux-tests-default: needs: [changes, generate-linux-launcher] timeout-minutes: 150 runs-on: ubuntu-24.04 env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.test_all == 'true' || needs.changes.outputs.test_native == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping native Linux integration tests (default) -- changes do not affect compiled code or CI." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "temurin:17" - uses: actions/download-artifact@v8 if: env.SHOULD_RUN == 'true' with: name: linux-launchers path: artifacts/ - name: Native integration tests if: env.SHOULD_RUN == 'true' run: ./mill -i nativeIntegrationTests env: UPDATE_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SCALA_CLI_IT_FORCED_LAUNCHER_DIRECTORY: artifacts/ SCALA_CLI_IT_GROUP: 1 SCALA_CLI_SODIUM_JNI_ALLOW: false - name: Convert Mill test reports to JUnit XML format if: (success() || failure()) && env.SHOULD_RUN == 'true' run: .github/scripts/generate-junit-reports.sc linux-tests-default 'Scala CLI Linux Tests (default)' test-report.xml out/ - name: Upload test report uses: actions/upload-artifact@v7 if: (success() || failure()) && env.SHOULD_RUN == 'true' with: name: test-results-linux-tests-default path: test-report.xml native-linux-tests-scala-2-13: needs: [changes, generate-linux-launcher] timeout-minutes: 150 runs-on: ubuntu-24.04 env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.test_all == 'true' || needs.changes.outputs.test_native == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping native Linux integration tests (Scala 2.13) -- changes do not affect compiled code or CI." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "temurin:17" - uses: actions/download-artifact@v8 if: env.SHOULD_RUN == 'true' with: name: linux-launchers path: artifacts/ - name: Native integration tests if: env.SHOULD_RUN == 'true' run: ./mill -i nativeIntegrationTests env: UPDATE_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SCALA_CLI_IT_FORCED_LAUNCHER_DIRECTORY: artifacts/ SCALA_CLI_IT_GROUP: 2 SCALA_CLI_SODIUM_JNI_ALLOW: false - name: Convert Mill test reports to JUnit XML format if: (success() || failure()) && env.SHOULD_RUN == 'true' run: .github/scripts/generate-junit-reports.sc linux-tests-scala-2-13 'Scala CLI Linux Tests (Scala 2.13)' test-report.xml out/ - name: Upload test report uses: actions/upload-artifact@v7 if: (success() || failure()) && env.SHOULD_RUN == 'true' with: name: test-results-linux-tests-scala-2-13 path: test-report.xml native-linux-tests-scala-2-12: needs: [changes, generate-linux-launcher] timeout-minutes: 150 runs-on: ubuntu-24.04 env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.test_all == 'true' || needs.changes.outputs.test_native == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping native Linux integration tests (Scala 2.12) -- changes do not affect compiled code or CI." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "temurin:17" - uses: actions/download-artifact@v8 if: env.SHOULD_RUN == 'true' with: name: linux-launchers path: artifacts/ - name: Native integration tests if: env.SHOULD_RUN == 'true' run: ./mill -i nativeIntegrationTests env: UPDATE_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SCALA_CLI_IT_FORCED_LAUNCHER_DIRECTORY: artifacts/ SCALA_CLI_IT_GROUP: 3 SCALA_CLI_SODIUM_JNI_ALLOW: false - name: Convert Mill test reports to JUnit XML format if: (success() || failure()) && env.SHOULD_RUN == 'true' run: .github/scripts/generate-junit-reports.sc linux-tests-scala-2-12 'Scala CLI Linux Tests (Scala 2.12)' test-report.xml out/ - name: Upload test report uses: actions/upload-artifact@v7 if: (success() || failure()) && env.SHOULD_RUN == 'true' with: name: test-results-linux-tests-scala-2-12 path: test-report.xml native-linux-tests-lts: needs: [changes, generate-linux-launcher] timeout-minutes: 150 runs-on: ubuntu-24.04 env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.test_all == 'true' || needs.changes.outputs.test_native == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping native Linux integration tests (Scala 3 LTS) -- changes do not affect compiled code or CI." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "temurin:17" - uses: actions/download-artifact@v8 if: env.SHOULD_RUN == 'true' with: name: linux-launchers path: artifacts/ - name: Native integration tests if: env.SHOULD_RUN == 'true' run: ./mill -i nativeIntegrationTests env: UPDATE_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SCALA_CLI_IT_FORCED_LAUNCHER_DIRECTORY: artifacts/ SCALA_CLI_IT_GROUP: 4 SCALA_CLI_SODIUM_JNI_ALLOW: false - name: Convert Mill test reports to JUnit XML format if: (success() || failure()) && env.SHOULD_RUN == 'true' run: .github/scripts/generate-junit-reports.sc linux-tests-lts 'Scala CLI Linux Tests (Scala 3 LTS)' test-report.xml out/ - name: Upload test report uses: actions/upload-artifact@v7 if: (success() || failure()) && env.SHOULD_RUN == 'true' with: name: test-results-linux-tests-lts path: test-report.xml native-linux-tests-rc: needs: [changes, generate-linux-launcher] timeout-minutes: 150 runs-on: ubuntu-24.04 env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.test_all == 'true' || needs.changes.outputs.test_native == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping native Linux integration tests (RC) -- changes do not affect compiled code or CI." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "temurin:17" - uses: actions/download-artifact@v8 if: env.SHOULD_RUN == 'true' with: name: linux-launchers path: artifacts/ - name: Native integration tests if: env.SHOULD_RUN == 'true' run: ./mill -i nativeIntegrationTests env: UPDATE_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SCALA_CLI_IT_FORCED_LAUNCHER_DIRECTORY: artifacts/ SCALA_CLI_IT_GROUP: 5 SCALA_CLI_SODIUM_JNI_ALLOW: false - name: Convert Mill test reports to JUnit XML format if: (success() || failure()) && env.SHOULD_RUN == 'true' run: .github/scripts/generate-junit-reports.sc linux-tests-rc 'Scala CLI Linux Tests (5)' test-report.xml out/ - name: Upload test report uses: actions/upload-artifact@v7 if: (success() || failure()) && env.SHOULD_RUN == 'true' with: name: test-results-linux-tests-rc path: test-report.xml generate-linux-arm64-native-launcher: needs: [changes] timeout-minutes: 120 runs-on: ubuntu-24.04-arm env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.test_all == 'true' || needs.changes.outputs.test_native == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping Linux ARM64 native launcher generation -- changes do not affect compiled code or CI." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "temurin:17" - name: Install build dependencies if: env.SHOULD_RUN == 'true' run: | sudo apt-get update -q -y sudo apt-get install -q -y build-essential libz-dev zlib1g-dev python3-pip - name: Generate native launcher if: env.SHOULD_RUN == 'true' run: .github/scripts/generate-native-image.sh - run: ./mill -i ci.setShouldPublish if: env.SHOULD_RUN == 'true' - name: Build OS packages if: env.SHOULD_PUBLISH == 'true' && env.SHOULD_RUN == 'true' run: .github/scripts/generate-os-packages.sh - name: Copy artifacts if: env.SHOULD_RUN == 'true' run: ./mill -i copyDefaultLauncher --directory artifacts/ - uses: actions/upload-artifact@v7 if: env.SHOULD_RUN == 'true' with: name: linux-aarch64-launchers path: artifacts/ if-no-files-found: error retention-days: 2 native-linux-arm64-tests-default: needs: [changes, generate-linux-arm64-native-launcher] timeout-minutes: 150 runs-on: ubuntu-24.04-arm env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.test_all == 'true' || needs.changes.outputs.test_native == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping native Linux ARM64 integration tests (default) -- changes do not affect compiled code or CI." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "temurin:17" - uses: actions/download-artifact@v8 if: env.SHOULD_RUN == 'true' with: name: linux-aarch64-launchers path: artifacts/ - name: Native integration tests if: env.SHOULD_RUN == 'true' run: ./mill -i nativeIntegrationTests env: UPDATE_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SCALA_CLI_IT_FORCED_LAUNCHER_DIRECTORY: artifacts/ SCALA_CLI_IT_GROUP: 1 SCALA_CLI_SODIUM_JNI_ALLOW: false - name: Convert Mill test reports to JUnit XML format if: (success() || failure()) && env.SHOULD_RUN == 'true' run: .github/scripts/generate-junit-reports.sc linux-tests-default 'Scala CLI Linux ARM 64 Tests (default)' test-report.xml out/ - name: Upload test report uses: actions/upload-artifact@v7 if: (success() || failure()) && env.SHOULD_RUN == 'true' with: name: test-results-linux-arm64-tests-default path: test-report.xml native-linux-arm64-tests-rc: needs: [changes, generate-linux-arm64-native-launcher] timeout-minutes: 150 runs-on: ubuntu-24.04-arm env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.test_all == 'true' || needs.changes.outputs.test_native == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping native Linux ARM64 integration tests (RC) -- changes do not affect compiled code or CI." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "temurin:17" - uses: actions/download-artifact@v8 if: env.SHOULD_RUN == 'true' with: name: linux-aarch64-launchers path: artifacts/ - name: Native integration tests if: env.SHOULD_RUN == 'true' run: ./mill -i nativeIntegrationTests env: UPDATE_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SCALA_CLI_IT_FORCED_LAUNCHER_DIRECTORY: artifacts/ SCALA_CLI_IT_GROUP: 5 SCALA_CLI_SODIUM_JNI_ALLOW: false - name: Convert Mill test reports to JUnit XML format if: (success() || failure()) && env.SHOULD_RUN == 'true' run: .github/scripts/generate-junit-reports.sc linux-tests-rc 'Scala CLI Linux ARM64 Tests (5)' test-report.xml out/ - name: Upload test report uses: actions/upload-artifact@v7 if: (success() || failure()) && env.SHOULD_RUN == 'true' with: name: test-results-linux-arm64-tests-rc path: test-report.xml generate-macos-launcher: needs: [changes] timeout-minutes: 120 runs-on: "macOS-15-intel" env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.test_all == 'true' || needs.changes.outputs.test_native == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping macOS native launcher generation -- changes do not affect compiled code or CI." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "temurin:17" - name: Ensure it's not running on aarch64 if: env.SHOULD_RUN == 'true' run: scala-cli -e 'assert(System.getProperty("os.arch") != "aarch64")' - name: Generate native launcher if: env.SHOULD_RUN == 'true' run: .github/scripts/generate-native-image.sh - run: ./mill -i ci.setShouldPublish if: env.SHOULD_RUN == 'true' - name: Build OS packages if: env.SHOULD_PUBLISH == 'true' && env.SHOULD_RUN == 'true' run: .github/scripts/generate-os-packages.sh - name: Copy artifacts if: env.SHOULD_RUN == 'true' run: ./mill -i copyDefaultLauncher --directory artifacts/ - uses: actions/upload-artifact@v7 if: env.SHOULD_RUN == 'true' with: name: macos-launchers path: artifacts/ if-no-files-found: error retention-days: 2 native-macos-tests-default: needs: [changes, generate-macos-launcher] timeout-minutes: 150 runs-on: "macOS-15-intel" env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.test_all == 'true' || needs.changes.outputs.test_native == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping native macOS integration tests (default) -- changes do not affect compiled code or CI." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "temurin:17" - name: Ensure it's not running on aarch64 if: env.SHOULD_RUN == 'true' run: scala-cli -e 'assert(System.getProperty("os.arch") != "aarch64")' - uses: actions/download-artifact@v8 if: env.SHOULD_RUN == 'true' with: name: macos-launchers path: artifacts/ - name: Native integration tests if: env.SHOULD_RUN == 'true' run: ./mill -i nativeIntegrationTests env: UPDATE_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SCALA_CLI_IT_FORCED_LAUNCHER_DIRECTORY: artifacts/ SCALA_CLI_IT_GROUP: 1 SCALA_CLI_SODIUM_JNI_ALLOW: false - name: Convert Mill test reports to JUnit XML format if: (success() || failure()) && env.SHOULD_RUN == 'true' run: .github/scripts/generate-junit-reports.sc macos-tests-default 'Scala CLI MacOS Tests (default)' test-report.xml out/ - name: Upload test report uses: actions/upload-artifact@v7 if: (success() || failure()) && env.SHOULD_RUN == 'true' with: name: test-results-macos-tests-default path: test-report.xml native-macos-tests-rc: needs: [changes, generate-macos-launcher] timeout-minutes: 150 runs-on: "macOS-15-intel" env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.test_all == 'true' || needs.changes.outputs.test_native == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping native macOS integration tests (RC) -- changes do not affect compiled code or CI." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "temurin:17" - name: Ensure it's not running on aarch64 if: env.SHOULD_RUN == 'true' run: scala-cli -e 'assert(System.getProperty("os.arch") != "aarch64")' - uses: actions/download-artifact@v8 if: env.SHOULD_RUN == 'true' with: name: macos-launchers path: artifacts/ - name: Native integration tests if: env.SHOULD_RUN == 'true' run: ./mill -i nativeIntegrationTests env: UPDATE_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SCALA_CLI_IT_FORCED_LAUNCHER_DIRECTORY: artifacts/ SCALA_CLI_IT_GROUP: 5 SCALA_CLI_SODIUM_JNI_ALLOW: false - name: Convert Mill test reports to JUnit XML format if: (success() || failure()) && env.SHOULD_RUN == 'true' run: .github/scripts/generate-junit-reports.sc macos-tests-rc 'Scala CLI MacOS Tests (5)' test-report.xml out/ - name: Upload test report uses: actions/upload-artifact@v7 if: (success() || failure()) && env.SHOULD_RUN == 'true' with: name: test-results-macos-tests-rc path: test-report.xml generate-macos-arm64-launcher: needs: [changes] timeout-minutes: 120 runs-on: "macOS-15" env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.test_all == 'true' || needs.changes.outputs.test_native == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping macOS ARM64 native launcher generation -- changes do not affect compiled code or CI." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-22.2.0/graalvm-ce-java17-darwin-aarch64-22.2.0.tar.gz" - name: Ensure it's running on aarch64 if: env.SHOULD_RUN == 'true' run: scala-cli -e 'assert(System.getProperty("os.arch") == "aarch64")' - name: Generate native launcher if: env.SHOULD_RUN == 'true' run: .github/scripts/generate-native-image.sh - run: ./mill -i ci.setShouldPublish if: env.SHOULD_RUN == 'true' - name: Build OS packages if: env.SHOULD_PUBLISH == 'true' && env.SHOULD_RUN == 'true' run: .github/scripts/generate-os-packages.sh - name: Copy artifacts if: env.SHOULD_RUN == 'true' run: ./mill -i copyDefaultLauncher --directory artifacts/ - uses: actions/upload-artifact@v7 if: env.SHOULD_RUN == 'true' with: name: macos-arm64-launchers path: artifacts/ if-no-files-found: error retention-days: 2 native-macos-arm64-tests-default: needs: [changes, generate-macos-arm64-launcher] timeout-minutes: 150 runs-on: "macOS-15" env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.test_all == 'true' || needs.changes.outputs.test_native == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping native macOS ARM64 integration tests (default) -- changes do not affect compiled code or CI." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-22.2.0/graalvm-ce-java17-darwin-aarch64-22.2.0.tar.gz" - name: Ensure it's running on aarch64 if: env.SHOULD_RUN == 'true' run: scala-cli -e 'assert(System.getProperty("os.arch") == "aarch64")' - uses: actions/download-artifact@v8 if: env.SHOULD_RUN == 'true' with: name: macos-arm64-launchers path: artifacts/ - name: Native integration tests if: env.SHOULD_RUN == 'true' run: ./mill -i nativeIntegrationTests env: UPDATE_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SCALA_CLI_IT_FORCED_LAUNCHER_DIRECTORY: artifacts/ SCALA_CLI_IT_GROUP: 1 SCALA_CLI_SODIUM_JNI_ALLOW: false - name: Convert Mill test reports to JUnit XML format if: (success() || failure()) && env.SHOULD_RUN == 'true' run: .github/scripts/generate-junit-reports.sc macos-arm64-tests-default 'Scala CLI MacOS ARM64 Tests (default)' test-report.xml out/ - name: Upload test report uses: actions/upload-artifact@v7 if: (success() || failure()) && env.SHOULD_RUN == 'true' with: name: test-results-macos-arm64-tests-default path: test-report.xml native-macos-arm64-tests-lts: needs: [changes, generate-macos-arm64-launcher] timeout-minutes: 150 runs-on: "macOS-15" env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.test_all == 'true' || needs.changes.outputs.test_native == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping native macOS ARM64 integration tests (Scala 3 LTS) -- changes do not affect compiled code or CI." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-22.2.0/graalvm-ce-java17-darwin-aarch64-22.2.0.tar.gz" - name: Ensure it's running on aarch64 if: env.SHOULD_RUN == 'true' run: scala-cli -e 'assert(System.getProperty("os.arch") == "aarch64")' - uses: actions/download-artifact@v8 if: env.SHOULD_RUN == 'true' with: name: macos-arm64-launchers path: artifacts/ - name: Native integration tests if: env.SHOULD_RUN == 'true' run: ./mill -i nativeIntegrationTests env: UPDATE_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SCALA_CLI_IT_FORCED_LAUNCHER_DIRECTORY: artifacts/ SCALA_CLI_IT_GROUP: 4 SCALA_CLI_SODIUM_JNI_ALLOW: false - name: Convert Mill test reports to JUnit XML format if: (success() || failure()) && env.SHOULD_RUN == 'true' run: .github/scripts/generate-junit-reports.sc macos-arm64-tests-lts 'Scala CLI MacOS ARM64 Tests (Scala 3 LTS)' test-report.xml out/ - name: Upload test report uses: actions/upload-artifact@v7 if: (success() || failure()) && env.SHOULD_RUN == 'true' with: name: test-results-macos-arm64-tests-lts path: test-report.xml native-macos-arm64-tests-rc: needs: [changes, generate-macos-arm64-launcher] timeout-minutes: 150 runs-on: "macOS-15" env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.test_all == 'true' || needs.changes.outputs.test_native == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping native macOS ARM64 integration tests (RC) -- changes do not affect compiled code or CI." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-22.2.0/graalvm-ce-java17-darwin-aarch64-22.2.0.tar.gz" - name: Ensure it's running on aarch64 if: env.SHOULD_RUN == 'true' run: scala-cli -e 'assert(System.getProperty("os.arch") == "aarch64")' - uses: actions/download-artifact@v8 if: env.SHOULD_RUN == 'true' with: name: macos-arm64-launchers path: artifacts/ - name: Native integration tests if: env.SHOULD_RUN == 'true' run: ./mill -i nativeIntegrationTests env: UPDATE_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SCALA_CLI_IT_FORCED_LAUNCHER_DIRECTORY: artifacts/ SCALA_CLI_IT_GROUP: 5 SCALA_CLI_SODIUM_JNI_ALLOW: false - name: Convert Mill test reports to JUnit XML format if: (success() || failure()) && env.SHOULD_RUN == 'true' run: .github/scripts/generate-junit-reports.sc macos-arm64-tests-rc 'Scala CLI MacOS ARM64 Tests (5)' test-report.xml out/ - name: Upload test report uses: actions/upload-artifact@v7 if: (success() || failure()) && env.SHOULD_RUN == 'true' with: name: test-results-macos-arm64-tests-rc path: test-report.xml generate-windows-launcher: needs: [changes] timeout-minutes: 120 runs-on: "windows-2025" env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.test_all == 'true' || needs.changes.outputs.test_native == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping Windows native launcher generation -- changes do not affect compiled code or CI." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - name: Import custom registry and verify if: env.SHOULD_RUN == 'true' uses: ./.github/actions/windows-reg-import with: reg-file: .github/ci/windows/custom.reg - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "temurin:17" - name: Get latest coursier launcher if: env.SHOULD_RUN == 'true' run: .github/scripts/get-latest-cs.sh shell: bash - name: Generate native launcher if: env.SHOULD_RUN == 'true' run: .github/scripts/generate-native-image.sh shell: bash - run: ./mill -i ci.setShouldPublish if: env.SHOULD_RUN == 'true' - name: Build OS packages if: env.SHOULD_PUBLISH == 'true' && env.SHOULD_RUN == 'true' run: .github/scripts/generate-os-packages.sh shell: bash - name: Copy artifacts if: env.SHOULD_RUN == 'true' run: ./mill -i copyDefaultLauncher --directory artifacts/ - uses: actions/upload-artifact@v7 if: env.SHOULD_RUN == 'true' with: name: windows-launchers path: artifacts/ if-no-files-found: error retention-days: 2 native-windows-tests-default: needs: [changes, generate-windows-launcher] timeout-minutes: 150 runs-on: "windows-2025" env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.test_all == 'true' || needs.changes.outputs.test_native == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping native Windows integration tests (default) -- changes do not affect compiled code or CI." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - name: Import custom registry and verify if: env.SHOULD_RUN == 'true' uses: ./.github/actions/windows-reg-import with: reg-file: .github/ci/windows/custom.reg - name: Set up Python if: env.SHOULD_RUN == 'true' uses: actions/setup-python@v6 with: python-version: "3.10" - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "temurin:17" - name: Get latest coursier launcher if: env.SHOULD_RUN == 'true' run: .github/scripts/get-latest-cs.sh shell: bash - uses: actions/download-artifact@v8 if: env.SHOULD_RUN == 'true' with: name: windows-launchers path: artifacts/ - name: Native integration tests if: env.SHOULD_RUN == 'true' run: ./mill -i nativeIntegrationTests env: COURSIER_JNI: force UPDATE_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SCALA_CLI_IT_FORCED_LAUNCHER_DIRECTORY: artifacts/ SCALA_CLI_IT_GROUP: 1 SCALA_CLI_SODIUM_JNI_ALLOW: false - name: Convert Mill test reports to JUnit XML format if: (success() || failure()) && env.SHOULD_RUN == 'true' run: scala-cli shebang .github/scripts/generate-junit-reports.sc windows-tests-default 'Scala CLI Windows Tests (default)' test-report.xml out/ - name: Upload test report uses: actions/upload-artifact@v7 if: (success() || failure()) && env.SHOULD_RUN == 'true' with: name: test-results-windows-tests-default path: test-report.xml native-windows-tests-lts: needs: [changes, generate-windows-launcher] timeout-minutes: 150 runs-on: "windows-2025" env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.test_all == 'true' || needs.changes.outputs.test_native == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping native Windows integration tests (Scala 3 LTS) -- changes do not affect compiled code or CI." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - name: Import custom registry and verify if: env.SHOULD_RUN == 'true' uses: ./.github/actions/windows-reg-import with: reg-file: .github/ci/windows/custom.reg - name: Set up Python if: env.SHOULD_RUN == 'true' uses: actions/setup-python@v6 with: python-version: "3.10" - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "temurin:17" - name: Get latest coursier launcher if: env.SHOULD_RUN == 'true' run: .github/scripts/get-latest-cs.sh shell: bash - uses: actions/download-artifact@v8 if: env.SHOULD_RUN == 'true' with: name: windows-launchers path: artifacts/ - name: Native integration tests if: env.SHOULD_RUN == 'true' run: ./mill -i nativeIntegrationTests env: COURSIER_JNI: force UPDATE_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SCALA_CLI_IT_FORCED_LAUNCHER_DIRECTORY: artifacts/ SCALA_CLI_IT_GROUP: 4 SCALA_CLI_SODIUM_JNI_ALLOW: false - name: Convert Mill test reports to JUnit XML format if: (success() || failure()) && env.SHOULD_RUN == 'true' run: scala-cli shebang .github/scripts/generate-junit-reports.sc windows-tests-lts 'Scala CLI Windows Tests (Scala 3 LTS)' test-report.xml out/ - name: Upload test report uses: actions/upload-artifact@v7 if: (success() || failure()) && env.SHOULD_RUN == 'true' with: name: test-results-windows-tests-lts path: test-report.xml native-windows-tests-rc: needs: [changes, generate-windows-launcher] timeout-minutes: 150 runs-on: "windows-2025" env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.test_all == 'true' || needs.changes.outputs.test_native == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping native Windows integration tests (RC) -- changes do not affect compiled code or CI." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - name: Import custom registry and verify if: env.SHOULD_RUN == 'true' uses: ./.github/actions/windows-reg-import with: reg-file: .github/ci/windows/custom.reg - name: Set up Python if: env.SHOULD_RUN == 'true' uses: actions/setup-python@v6 with: python-version: "3.10" - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "temurin:17" - name: Get latest coursier launcher if: env.SHOULD_RUN == 'true' run: .github/scripts/get-latest-cs.sh shell: bash - uses: actions/download-artifact@v8 if: env.SHOULD_RUN == 'true' with: name: windows-launchers path: artifacts/ - name: Native integration tests if: env.SHOULD_RUN == 'true' run: ./mill -i nativeIntegrationTests env: COURSIER_JNI: force UPDATE_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SCALA_CLI_IT_FORCED_LAUNCHER_DIRECTORY: artifacts/ SCALA_CLI_IT_GROUP: 5 SCALA_CLI_SODIUM_JNI_ALLOW: false - name: Convert Mill test reports to JUnit XML format if: (success() || failure()) && env.SHOULD_RUN == 'true' run: scala-cli shebang .github/scripts/generate-junit-reports.sc windows-tests-rc 'Scala CLI Windows Tests (5)' test-report.xml out/ - name: Upload test report uses: actions/upload-artifact@v7 if: (success() || failure()) && env.SHOULD_RUN == 'true' with: name: test-results-windows-tests-rc path: test-report.xml generate-mostly-static-launcher: needs: [changes] timeout-minutes: 120 runs-on: ubuntu-24.04 env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.test_all == 'true' || needs.changes.outputs.test_native == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping mostly-static native launcher generation -- changes do not affect compiled code or CI." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "temurin:17" - name: Generate native launcher if: env.SHOULD_RUN == 'true' run: .github/scripts/generate-native-image.sh mostly-static shell: bash - name: Copy artifacts if: env.SHOULD_RUN == 'true' run: ./mill -i copyMostlyStaticLauncher --directory artifacts/ - uses: actions/upload-artifact@v7 if: env.SHOULD_RUN == 'true' with: name: mostly-static-launchers path: artifacts/ if-no-files-found: error retention-days: 2 native-mostly-static-tests-default: needs: [changes, generate-mostly-static-launcher] timeout-minutes: 150 runs-on: ubuntu-24.04 env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.test_all == 'true' || needs.changes.outputs.test_native == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping native mostly-static integration tests (default) -- changes do not affect compiled code or CI." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "temurin:17" - uses: actions/download-artifact@v8 if: env.SHOULD_RUN == 'true' with: name: mostly-static-launchers path: artifacts/ - name: Build slim docker image if: env.SHOULD_RUN == 'true' run: .github/scripts/generate-slim-docker-image.sh - name: Native integration tests if: env.SHOULD_RUN == 'true' run: ./mill -i integration.test.nativeMostlyStatic env: UPDATE_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SCALA_CLI_IT_FORCED_LAUNCHER_DIRECTORY: artifacts/ SCALA_CLI_IT_GROUP: 1 SCALA_CLI_SODIUM_JNI_ALLOW: false - name: Docker integration tests if: (success() || failure()) && env.SHOULD_RUN == 'true' run: ./mill integration.docker-slim.test - name: Convert Mill test reports to JUnit XML format if: (success() || failure()) && env.SHOULD_RUN == 'true' run: .github/scripts/generate-junit-reports.sc native-mostly-static-tests-default 'Scala CLI Native Mostly Static Tests (default)' test-report.xml out/ - name: Upload test report uses: actions/upload-artifact@v7 if: (success() || failure()) && env.SHOULD_RUN == 'true' with: name: test-results-native-mostly-static-tests-default path: test-report.xml - name: Login to GitHub Container Registry if: startsWith(github.ref, 'refs/tags/v') && env.SHOULD_RUN == 'true' uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Push slim scala-cli image to github container registry if: startsWith(github.ref, 'refs/tags/v') && env.SHOULD_RUN == 'true' run: .github/scripts/publish-slim-docker-images.sh native-mostly-static-tests-rc: needs: [changes, generate-mostly-static-launcher] timeout-minutes: 150 runs-on: ubuntu-24.04 env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.test_all == 'true' || needs.changes.outputs.test_native == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping native mostly-static integration tests (RC) -- changes do not affect compiled code or CI." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "temurin:17" - uses: actions/download-artifact@v8 if: env.SHOULD_RUN == 'true' with: name: mostly-static-launchers path: artifacts/ - name: Native integration tests if: env.SHOULD_RUN == 'true' run: ./mill -i integration.test.nativeMostlyStatic env: UPDATE_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SCALA_CLI_IT_FORCED_LAUNCHER_DIRECTORY: artifacts/ SCALA_CLI_IT_GROUP: 5 SCALA_CLI_SODIUM_JNI_ALLOW: false - name: Convert Mill test reports to JUnit XML format if: (success() || failure()) && env.SHOULD_RUN == 'true' run: .github/scripts/generate-junit-reports.sc native-mostly-static-tests-rc 'Scala CLI Native Mostly Static Tests (5)' test-report.xml out/ - name: Upload test report uses: actions/upload-artifact@v7 if: (success() || failure()) && env.SHOULD_RUN == 'true' with: name: test-results-native-mostly-static-tests-rc path: test-report.xml generate-static-launcher: needs: [changes] timeout-minutes: 120 runs-on: ubuntu-24.04 env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.test_all == 'true' || needs.changes.outputs.test_native == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping static native launcher generation -- changes do not affect compiled code or CI." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "temurin:17" - name: Generate native launcher if: env.SHOULD_RUN == 'true' run: .github/scripts/generate-native-image.sh static shell: bash - name: Copy artifacts if: env.SHOULD_RUN == 'true' run: ./mill -i copyStaticLauncher --directory artifacts/ - uses: actions/upload-artifact@v7 if: env.SHOULD_RUN == 'true' with: name: static-launchers path: artifacts/ if-no-files-found: error retention-days: 2 native-static-tests-default: needs: [changes, generate-static-launcher] timeout-minutes: 150 runs-on: ubuntu-24.04 env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.test_all == 'true' || needs.changes.outputs.test_native == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping native static integration tests (default) -- changes do not affect compiled code or CI." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "temurin:17" - uses: actions/download-artifact@v8 if: env.SHOULD_RUN == 'true' with: name: static-launchers path: artifacts/ - name: Build docker image if: env.SHOULD_RUN == 'true' run: .github/scripts/generate-docker-image.sh - name: Native integration tests if: env.SHOULD_RUN == 'true' run: ./mill -i integration.test.nativeStatic env: UPDATE_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SCALA_CLI_IT_FORCED_LAUNCHER_DIRECTORY: artifacts/ SCALA_CLI_IT_GROUP: 1 SCALA_CLI_SODIUM_JNI_ALLOW: false - name: Docker integration tests if: (success() || failure()) && env.SHOULD_RUN == 'true' run: ./mill integration.docker.test - name: Convert Mill test reports to JUnit XML format if: (success() || failure()) && env.SHOULD_RUN == 'true' run: .github/scripts/generate-junit-reports.sc native-static-tests-default 'Scala CLI Native Static Tests (default)' test-report.xml out/ - name: Upload test report uses: actions/upload-artifact@v7 if: (success() || failure()) && env.SHOULD_RUN == 'true' with: name: test-results-native-static-tests-default path: test-report.xml - name: Login to GitHub Container Registry if: startsWith(github.ref, 'refs/tags/v') && env.SHOULD_RUN == 'true' uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Push scala-cli to github container registry if: startsWith(github.ref, 'refs/tags/v') && env.SHOULD_RUN == 'true' run: .github/scripts/publish-docker-images.sh native-static-tests-rc: needs: [changes, generate-static-launcher] timeout-minutes: 150 runs-on: ubuntu-24.04 env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.test_all == 'true' || needs.changes.outputs.test_native == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping native static integration tests (RC) -- changes do not affect compiled code or CI." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "temurin:17" - uses: actions/download-artifact@v8 if: env.SHOULD_RUN == 'true' with: name: static-launchers path: artifacts/ - name: Build docker image if: env.SHOULD_RUN == 'true' run: .github/scripts/generate-docker-image.sh - name: Native integration tests if: env.SHOULD_RUN == 'true' run: ./mill -i integration.test.nativeStatic env: UPDATE_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SCALA_CLI_IT_FORCED_LAUNCHER_DIRECTORY: artifacts/ SCALA_CLI_IT_GROUP: 5 SCALA_CLI_SODIUM_JNI_ALLOW: false - name: Convert Mill test reports to JUnit XML format if: (success() || failure()) && env.SHOULD_RUN == 'true' run: .github/scripts/generate-junit-reports.sc native-static-tests-rc 'Scala CLI Native Static Tests (5)' test-report.xml out/ - name: Upload test report uses: actions/upload-artifact@v7 if: (success() || failure()) && env.SHOULD_RUN == 'true' with: name: test-results-native-static-tests-rc path: test-report.xml docs-tests: needs: [changes] # for now, let's run those tests only on ubuntu runs-on: ubuntu-24.04 env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.docs == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.gifs == 'true' || needs.changes.outputs.test_all == 'true' || needs.changes.outputs.test_docs == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping docs tests -- changes do not affect code, docs, CI, or gifs." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "zulu:17" - uses: actions/setup-node@v6 if: env.SHOULD_RUN == 'true' with: node-version: 24 - name: Build documentation if: env.SHOULD_RUN == 'true' run: .github/scripts/build-website.sh - name: Verify release notes formatting if: env.SHOULD_RUN == 'true' run: .github/scripts/process_release_notes.sc verify website/docs/release_notes.md - name: Test documentation if: env.SHOULD_RUN == 'true' run: ./mill -i 'docs-tests[]'.test - name: Convert Mill test reports to JUnit XML format if: (success() || failure()) && env.SHOULD_RUN == 'true' run: .github/scripts/generate-junit-reports.sc docs-tests 'Scala CLI Docs Tests' test-report.xml out/ - name: Upload test report uses: actions/upload-artifact@v7 if: (success() || failure()) && env.SHOULD_RUN == 'true' with: name: test-results-docs-tests path: test-report.xml checks: needs: [changes] timeout-minutes: 60 runs-on: ubuntu-24.04 env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.docs == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.format_config == 'true' || needs.changes.outputs.test_all == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping checks -- changes do not affect code, docs, CI, or format config." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "temurin:17" - name: Check Scala / Scala.js versions in doc if: env.SHOULD_RUN == 'true' run: ./mill -i ci.checkScalaVersions - name: Check native-image config format if: env.SHOULD_RUN == 'true' run: ./mill -i __.checkNativeImageConfFormat - name: Check Ammonite availability if: env.SHOULD_RUN == 'true' run: ./mill -i 'dummy.amm[_].resolvedRunMvnDeps' - name: Check for cross Scala version conflicts if: env.SHOULD_RUN == 'true' run: .github/scripts/check-cross-version-deps.sc - name: Scalafix check if: env.SHOULD_RUN == 'true' run: | ./mill -i __.fix --check || ( echo "To remove unused import run" echo " ./mill -i __.fix" exit 1 ) format: needs: [changes] timeout-minutes: 15 runs-on: ubuntu-24.04 env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.docs == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.format_config == 'true' || needs.changes.outputs.test_all == 'true' || needs.changes.outputs.test_format == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping format check -- changes do not affect code, docs, CI, or format config." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' - run: scala-cli fmt . --check if: env.SHOULD_RUN == 'true' reference-doc: needs: [changes] timeout-minutes: 15 runs-on: ubuntu-24.04 env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.docs == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.test_all == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping reference doc check -- changes do not affect code, docs, or CI." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "temurin:17" - name: Check that reference doc is up-to-date if: env.SHOULD_RUN == 'true' run: | ./mill -i 'generate-reference-doc[]'.run --check || ( echo "Reference doc is not up-to-date. Run" echo " ./mill -i 'generate-reference-doc[]'.run" echo "to update it, then commit the result." exit 1 ) bloop-memory-footprint: needs: [changes] timeout-minutes: 120 runs-on: ubuntu-24.04 env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.benchmark == 'true' || needs.changes.outputs.test_all == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping bloop memory footprint benchmark -- changes do not affect code, CI, or benchmarks." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "temurin:17" - name: Java Version if: env.SHOULD_RUN == 'true' run: java -version - name: Java Home if: env.SHOULD_RUN == 'true' run: echo "$JAVA_HOME" - name: Build Scala CLI if: env.SHOULD_RUN == 'true' run: ./mill copyJvmLauncher --directory build - name: Build Benchmark if: env.SHOULD_RUN == 'true' run: java -jar ./build/scala-cli --power package --standalone gcbenchmark/gcbenchmark.scala -o gc - name: Run Benchmark if: env.SHOULD_RUN == 'true' run: ./gc $(realpath ./build/scala-cli) test-hypothetical-sbt-export: needs: [changes] timeout-minutes: 120 runs-on: ubuntu-24.04 env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.test_all == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping sbt export test -- changes do not affect compiled code or CI." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "temurin:17" - name: Try to export to SBT if: env.SHOULD_RUN == 'true' run: scala-cli --power export --sbt . vc-redist: needs: [changes] timeout-minutes: 15 runs-on: "windows-2025" if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == 'Virtuslab/scala-cli' env: SHOULD_RUN: ${{ needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.test_all == 'true' }} steps: - name: Log skip reason if: env.SHOULD_RUN != 'true' run: echo "Skipping vc-redist -- changes do not affect compiled code or CI." - uses: actions/checkout@v6 if: env.SHOULD_RUN == 'true' with: fetch-depth: 0 submodules: true - name: Import custom registry and verify if: env.SHOULD_RUN == 'true' uses: ./.github/actions/windows-reg-import with: reg-file: .github/ci/windows/custom.reg - uses: coursier/cache-action@v8 if: env.SHOULD_RUN == 'true' with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 if: env.SHOULD_RUN == 'true' with: jvm: "temurin:17" - run: ./mill -i ci.copyVcRedist if: env.SHOULD_RUN == 'true' - uses: actions/upload-artifact@v7 if: env.SHOULD_RUN == 'true' with: name: vc-redist-launchers path: artifacts/ if-no-files-found: warn retention-days: 2 publish: needs: - changes - unit-tests - jvm-bootstrapped-tests-default - jvm-tests-default - jvm-tests-scala-2-13 - jvm-tests-scala-2-12 - jvm-tests-lts - jvm-tests-rc - native-linux-tests-default - native-linux-tests-scala-2-13 - native-linux-tests-scala-2-12 - native-linux-tests-lts - native-linux-tests-rc - native-linux-arm64-tests-default - native-linux-arm64-tests-rc - native-macos-tests-default - native-macos-tests-rc - native-macos-arm64-tests-default - native-macos-arm64-tests-lts - native-macos-arm64-tests-rc - native-windows-tests-default - native-windows-tests-lts - native-windows-tests-rc - native-mostly-static-tests-default - native-mostly-static-tests-rc - native-static-tests-default - native-static-tests-rc - vc-redist - format - checks - test-fish-shell - test-hypothetical-sbt-export - bloop-memory-footprint - reference-doc - docs-tests if: github.event_name == 'push' && github.repository == 'VirtusLab/scala-cli' runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v6 with: fetch-depth: 0 submodules: true ssh-key: ${{ secrets.SSH_PRIVATE_KEY_SCALA_CLI }} - uses: coursier/cache-action@v8 with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 with: jvm: "temurin:17" - name: GPG setup run: .github/scripts/gpg-setup.sh env: PGP_SECRET: ${{ secrets.PGP_SECRET }} - name: Set publish flag run: ./mill -i ci.setShouldPublish - name: Print version run: ./mill -i show project.publish.finalPublishVersion - name: Publish to Sonatype Central run: ./mill mill.scalalib.SonatypeCentralPublishModule/ --publishArtifacts '{__[],_}.publishArtifacts' if: env.SHOULD_PUBLISH == 'true' env: MILL_PGP_SECRET_BASE64: ${{ secrets.PGP_SECRET }} MILL_PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} MILL_SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} MILL_SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} - uses: webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555 with: ssh-private-key: | ${{ secrets.SSH_PRIVATE_KEY_SCALA_CLI }} - name: Update stable branch if: env.SHOULD_PUBLISH == 'true' && startsWith(github.ref, 'refs/tags/v') run: | git config user.name gh-actions git config user.email actions@github.com git checkout stable git merge origin/main -m "Back port of documentation changes to stable" git push origin stable launchers: timeout-minutes: 20 needs: - changes - unit-tests - jvm-bootstrapped-tests-default - jvm-tests-default - jvm-tests-scala-2-13 - jvm-tests-scala-2-12 - jvm-tests-lts - jvm-tests-rc - native-linux-tests-default - native-linux-tests-scala-2-13 - native-linux-tests-scala-2-12 - native-linux-tests-lts - native-linux-tests-rc - native-linux-arm64-tests-default - native-linux-arm64-tests-rc - native-macos-tests-default - native-macos-tests-rc - native-macos-arm64-tests-default - native-macos-arm64-tests-lts - native-macos-arm64-tests-rc - native-windows-tests-default - native-windows-tests-lts - native-windows-tests-rc - native-mostly-static-tests-default - native-mostly-static-tests-rc - native-static-tests-default - native-static-tests-rc - vc-redist - format - checks - test-fish-shell - test-hypothetical-sbt-export - bloop-memory-footprint - reference-doc - generate-linux-arm64-native-launcher - publish if: github.event_name == 'push' runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v6 with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 with: jvm: "temurin:17" - run: ./mill -i ci.setShouldPublish - uses: actions/download-artifact@v8 if: env.SHOULD_PUBLISH == 'true' with: name: linux-launchers path: artifacts/ - uses: actions/download-artifact@v8 if: env.SHOULD_PUBLISH == 'true' with: name: linux-aarch64-launchers path: artifacts/ - uses: actions/download-artifact@v8 if: env.SHOULD_PUBLISH == 'true' with: name: macos-launchers path: artifacts/ - uses: actions/download-artifact@v8 if: env.SHOULD_PUBLISH == 'true' with: name: macos-arm64-launchers path: artifacts/ - uses: actions/download-artifact@v8 if: env.SHOULD_PUBLISH == 'true' with: name: windows-launchers path: artifacts/ - uses: actions/download-artifact@v8 if: env.SHOULD_PUBLISH == 'true' with: name: mostly-static-launchers path: artifacts/ - uses: actions/download-artifact@v8 if: env.SHOULD_PUBLISH == 'true' with: name: static-launchers path: artifacts/ - uses: actions/download-artifact@v8 if: env.SHOULD_PUBLISH == 'true' with: name: jvm-launchers path: artifacts/ - uses: actions/download-artifact@v8 if: env.SHOULD_PUBLISH == 'true' with: name: vc-redist-launchers path: artifacts/ - run: ./mill -i uploadLaunchers --directory artifacts/ if: env.SHOULD_PUBLISH == 'true' env: UPLOAD_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} update-packages: name: Update packages needs: - changes - launchers - publish runs-on: ubuntu-24.04 if: startsWith(github.ref, 'refs/tags/v') && github.repository == 'VirtusLab/scala-cli' steps: - uses: actions/checkout@v6 with: fetch-depth: 0 submodules: true - uses: coursier/cache-action@v8 with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 with: jvm: "temurin:17" - uses: actions/download-artifact@v8 with: name: linux-launchers path: artifacts/ - uses: actions/download-artifact@v8 with: name: linux-aarch64-launchers path: artifacts/ - uses: actions/download-artifact@v8 with: name: macos-launchers path: artifacts/ - uses: actions/download-artifact@v8 with: name: macos-arm64-launchers path: artifacts/ - uses: actions/download-artifact@v8 with: name: windows-launchers path: artifacts/ - uses: actions/download-artifact@v8 with: name: mostly-static-launchers path: artifacts/ - uses: actions/download-artifact@v8 with: name: static-launchers path: artifacts/ - uses: actions/download-artifact@v8 with: name: jvm-launchers path: artifacts/ - name: Display structure of downloaded files run: ls -R working-directory: artifacts/ - uses: webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555 with: ssh-private-key: | ${{ secrets.SCALA_CLI_PACKAGES_KEY }} ${{ secrets.HOMEBREW_SCALA_CLI_KEY }} ${{ secrets.HOMEBREW_SCALA_EXPERIMENTAL_KEY }} ${{ secrets.SCALA_CLI_SETUP_KEY }} - run: ./mill -i ci.updateInstallationScript continue-on-error: true - run: ./mill -i ci.updateScalaCliBrewFormula continue-on-error: true - name: GPG setup run: .github/scripts/gpg-setup.sh env: PGP_SECRET: ${{ secrets.PGP_SECRET }} - run: ./mill -i ci.updateDebianPackages continue-on-error: true env: PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} GPG_EMAIL: ${{ secrets.GPG_EMAIL }} - run: ./mill -i ci.updateCentOsPackages continue-on-error: true env: PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} KEYGRIP: ${{ secrets.KEYGRIP }} PGP_SECRET: ${{ secrets.PGP_SECRET }} GPG_EMAIL: ${{ secrets.GPG_EMAIL }} - run: ./mill -i ci.updateStandaloneLauncher continue-on-error: true env: UPLOAD_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Publish to SDKMAN continue-on-error: true run: .github/scripts/publish-sdkman.sh shell: bash env: SDKMAN_KEY: ${{ secrets.SDKMAN_KEY }} SDKMAN_TOKEN: ${{ secrets.SDKMAN_TOKEN }} - run: ./mill -i ci.updateScalaCliSetup continue-on-error: true - run: ./mill -i ci.updateScalaExperimentalBrewFormula update-windows-packages: name: Update Windows packages needs: - changes - launchers - publish runs-on: "windows-2025" if: startsWith(github.ref, 'refs/tags/v') && github.repository == 'VirtusLab/scala-cli' steps: - uses: actions/checkout@v6 with: fetch-depth: 0 submodules: true - name: Import custom registry and verify uses: ./.github/actions/windows-reg-import with: reg-file: .github/ci/windows/custom.reg - uses: coursier/cache-action@v8 with: ignoreJob: true - uses: VirtusLab/scala-cli-setup@v1 with: jvm: "temurin:17" - uses: actions/download-artifact@v8 with: name: windows-launchers path: artifacts/ - name: Publish to chocolatey run: ./mill -i ci.updateChocolateyPackage continue-on-error: true env: CHOCO_SECRET: ${{ secrets.CHOCO_SECRET_KEY }} - uses: vedantmgoyal9/winget-releaser@main with: identifier: VirtusLab.ScalaCLI installers-regex: '\.msi$' fork-user: scala-steward token: ${{ secrets.STEWARD_WINGET_TOKEN }} ================================================ FILE: .github/workflows/publish-docker.yml ================================================ name: Create and publish a Docker image concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true on: push: branches: ["main"] tags: ["v*"] env: REGISTRY: ghcr.io IMAGE_NAME: virtuslab/scala-cli DOCKERFILE: ./Dockerfile REGISTRY_LOGIN: ${{ github.actor }} REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} jobs: docker_build: strategy: fail-fast: true matrix: os: ["ubuntu-24.04", "ubuntu-24.04-arm"] runs-on: ${{ matrix.os }} # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. permissions: contents: read packages: write attestations: write id-token: write # steps: - name: Checkout repository uses: actions/checkout@v6 - name: Log in to the Container registry uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY }} username: ${{ env.REGISTRY_LOGIN }} password: ${{ env.REGISTRY_PASSWORD }} # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels. - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@v6 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages. # It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository. # It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step. - name: Build and push Docker image id: push uses: docker/build-push-action@v7 with: context: . file: ${{ env.DOCKERFILE }} cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-${{ matrix.os }} cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-${{ matrix.os }},mode=max push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} # This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see "[AUTOTITLE](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds)." - name: Generate artifact attestation uses: actions/attest-build-provenance@v4 with: subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}} subject-digest: ${{ steps.push.outputs.digest }} push-to-registry: true - name: Export digest run: | mkdir -p /tmp/digests digest="${{ steps.push.outputs.digest }}" touch "/tmp/digests/${digest#sha256:}" - name: Upload digest uses: actions/upload-artifact@v7 with: name: digests-${{ matrix.os }} path: /tmp/digests/* if-no-files-found: error retention-days: 1 docker_release_merge: runs-on: ubuntu-24.04 permissions: contents: read packages: write attestations: write id-token: write needs: [docker_build] if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') steps: - name: Download digests uses: actions/download-artifact@v8 with: pattern: digests-* path: /tmp/digests merge-multiple: true - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 - name: Log in to the Container registry uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY }} username: ${{ env.REGISTRY_LOGIN }} password: ${{ env.REGISTRY_PASSWORD }} - name: Docker meta id: meta uses: docker/metadata-action@v6 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - name: Create manifest list and push working-directory: /tmp/digests run: | docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ $(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *) - name: Inspect image run: | docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} ================================================ FILE: .github/workflows/test-report.yml ================================================ name: 'Test Report' on: workflow_run: workflows: ['CI'] types: - completed permissions: statuses: write checks: write contents: write pull-requests: write actions: write jobs: report: runs-on: ubuntu-latest steps: - uses: dorny/test-reporter@v3 with: artifact: /test-results-(.*)/ name: 'Test report $1' path: '*.xml' reporter: java-junit ================================================ FILE: .github/workflows/website.yaml ================================================ name: Website deploy on: push: branches: - stable jobs: update-website: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: fetch-depth: 0 submodules: true - uses: actions/setup-node@v6 with: node-version: 24 - run: .github/scripts/update-website.sh env: GIT_USER: Virtuslab DEPLOYMENT_BRANCH: gh-pages GIT_PASS: ${{ secrets.GITHUB_TOKEN }} # after the release the PR should be empty - name: Open PR with changes back to main uses: repo-sync/pull-request@v2 with: destination_branch: "main" github_token: ${{ secrets.GITHUB_TOKEN }} pr_title: "Back port of documentation changes to main" name: Update stable branch branch: backport/stable ================================================ FILE: .gitignore ================================================ out/ .bloop/ .metals/ .vscode/ .idea/ .cursor/ .bsp .scala-build dest/ target/ */scoverage.coverage # ignore vim backup files *.sw[op] .DS_Store ================================================ FILE: .mill-jvm-opts ================================================ -Xmx2048m -Xms128m -Xss8m -Dxsbt.skip.cp.lookup=true ================================================ FILE: .mill-version ================================================ 1.1.5 ================================================ FILE: .scala-steward.conf ================================================ postUpdateHooks = [{ command = ["./mill", "-i", "generate-reference-doc[].run"], commitMessage = "Generate the reference doc" }] ================================================ FILE: .scalafix.conf ================================================ rules = [ DisableSyntax, RemoveUnused, OrganizeImports, NoValInForComprehension, ProcedureSyntax ] DisableSyntax.noFinalize = true DisableSyntax.noIsInstanceOf = true DisableSyntax.noReturns = true // `rules` on compilation triggered.rules = [ DisableSyntax ] OrganizeImports { coalesceToWildcardImportThreshold = 6 expandRelative = true groups = ["*", "re:javax?\\.", "scala."] groupedImports = AggressiveMerge } ================================================ FILE: .scalafix3.conf ================================================ rules = [ DisableSyntax, RemoveUnused, OrganizeImports, NoValInForComprehension, # ProcedureSyntax ] DisableSyntax.noFinalize = true DisableSyntax.noIsInstanceOf = true DisableSyntax.noReturns = true // `rules` on compilation triggered.rules = [ DisableSyntax ] OrganizeImports { coalesceToWildcardImportThreshold = 6 expandRelative = true groups = ["*", "re:javax?\\.", "scala."] groupedImports = AggressiveMerge targetDialect = Scala3 } ================================================ FILE: .scalafmt.conf ================================================ version = "3.10.7" align.preset = more maxColumn = 100 assumeStandardLibraryStripMargin = true indent.defnSite = 2 indentOperator.topLevelOnly = false align.preset = more align.openParenCallSite = false newlines.source = keep newlines.beforeMultiline = keep newlines.afterCurlyLambdaParams = keep newlines.alwaysBeforeElseAfterCurlyIf = true runner.dialect = scala3 rewrite.rules = [ RedundantBraces RedundantParens SortModifiers ] rewrite.redundantBraces { ifElseExpressions = true includeUnitMethods = false stringInterpolation = true } rewrite.sortModifiers.order = [ "private", "final", "override", "protected", "implicit", "sealed", "abstract", "lazy" ] project.excludeFilters = [ ".bloop" ".metals" ".scala-build" "examples" # Scala 3 scripts and using directives not supported yet "out" "scala-version.scala" ] ================================================ FILE: AGENTS.md ================================================ # AGENTS.md — Guidance for AI agents contributing to Scala CLI Short reference for AI agents. For task-specific guidance (directives, integration tests), load skills from * *[agentskills/](agentskills/)** when relevant. > **LLM Policy**: All AI-assisted contributions must comply with the > [LLM usage policy](https://github.com/scala/scala3/blob/HEAD/LLM_POLICY.md). The contributor (human) is responsible > for every line. State LLM usage in the PR description. See [LLM_POLICY.md](LLM_POLICY.md). ## Human-facing docs - **[DEV.md](DEV.md)** — Setup, run from source, tests, launchers, GraalVM. - **[CONTRIBUTING.md](CONTRIBUTING.md)** — PR workflow, formatting, reference doc generation. - **[INTERNALS.md](INTERNALS.md)** — Modules, `Inputs → Sources → Build`, preprocessing. ## Build system The project uses [Mill](https://mill-build.org/). Mill launchers ship with the repo (`./mill`). JVM 17 required. Cross-compilation: default `Scala.defaultInternal`; `[]` = default version, `[_]` = all. ### Key build files | File | Purpose | |---------------------------------|------------------------------------------------------------------------------------------| | `build.mill` | Root build definition: all module declarations, CI helper tasks, integration test wiring | | `project/deps/package.mill` | Dependency versions and definitions (`Deps`, `Scala`, `Java` objects) | | `project/settings/package.mill` | Shared traits, utils (`HasTests`, `CliLaunchers`, `FormatNativeImageConf`, etc.) | | `project/publish/package.mill` | Publishing settings | | `project/website/package.mill` | Website-related build tasks | ### Essential commands ```bash ./mill -i clean # Clean Mill context ./mill -i scala …args… # Run Scala CLI from source ./mill -i __.compile # Compile everything ./mill -i unitTests # All unit tests ./mill -i 'build-module[].test' # Unit tests for a specific module ./mill -i 'build-module[].test' 'scala.build.tests.BuildTestsScalac.*' # Filter by suite ./mill -i 'build-module[].test' 'scala.build.tests.BuildTests.simple' # Single test by name ./mill -i integration.test.jvm # Integration tests (JVM launcher) ./mill -i integration.test.jvm 'scala.cli.integration.RunTestsDefault.*' # Integration: filter by suite ./mill -i 'generate-reference-doc[]'.run # Regenerate reference docs ./mill -i __.fix # Fix import ordering (scalafix) scala-cli fmt . # Format all code (scalafmt) ``` ## Project modules Modules live under `modules/`. The dependency graph flows roughly as: ``` specification-level → config → core → options → directives → build-module → cli ``` ### Module overview The list below may not be exhaustive — check `modules/` and `build.mill` for the current set. | Module | Purpose | |-----------------------------------------------|------------------------------------------------------------------------------------------------------------------| | `specification-level` | Defines `SpecificationLevel` (MUST / SHOULD / IMPLEMENTATION / RESTRICTED / EXPERIMENTAL) for SIP-46 compliance. | | `config` | Scala CLI configuration keys and persistence. | | `build-macros` | Compile-time macros (e.g. `EitherCps`). | | `core` | Core types: `Inputs`, `Sources`, build constants, Bloop integration, JVM/JS/Native tooling. | | `options` | `BuildOptions`, `SharedOptions`, and all option types. | | `directives` | Using directive handlers — the bridge between `//> using` directives and `BuildOptions`. | | `build-module` (aliased from `build` in mill) | The main build pipeline: preprocessing, compilation, post-processing. Most business logic lives here. | | `cli` | Command definitions, argument parsing (CaseApp), the `ScalaCli` entry point. Packaged as the native image. | | `runner` | Lightweight app that runs a main class and pretty-prints exceptions. Fetched at runtime. | | `test-runner` | Discovers and runs test frameworks/suites. Fetched at runtime. | | `tasty-lib` | Edits file names in `.tasty` files for source mapping. | | `scala-cli-bsp` | BSP protocol types. | | `integration` | Integration tests (see dedicated section below). | | `docs-tests` | Tests that validate documentation (`Sclicheck`). | | `generate-reference-doc` | Generates reference documentation from CLI option/directive metadata. | ## Specification levels Every command, CLI option, and using directive has a `SpecificationLevel`. This is central to how features are exposed. | Level | In the Scala Runner spec? | Available without `--power`? | Stability | |------------------|---------------------------|------------------------------|---------------------------------| | `MUST` | Yes | Yes | Stable | | `SHOULD` | Yes | Yes | Stable | | `IMPLEMENTATION` | No | Yes | Stable | | `RESTRICTED` | No | No (requires `--power`) | Stable | | `EXPERIMENTAL` | No | No (requires `--power`) | Unstable — may change/disappear | **New features contributed by agents should generally be marked `EXPERIMENTAL`** unless the maintainers explicitly request otherwise. This applies to new sub-commands, options, and directives alike. The specification level is set via: - **Directives**: `@DirectiveLevel(SpecificationLevel.EXPERIMENTAL)` annotation on the directive case class. - **CLI options**: `@Tag(tags.experimental)` annotation on option fields. - **Commands**: Override `scalaSpecificationLevel` in the command class. ## Using directives Using directives are in-source configuration comments: ```scala //> using scala 3 //> using dep com.lihaoyi::os-lib:0.11.4 //> using test.dep org.scalameta::munit::1.1.1 ``` Directives are parsed by `using_directives`, then `ExtractedDirectives` → `DirectivesPreprocessor` → `BuildOptions`/ `BuildRequirements`. **CLI options override directive values.** To add a new directive, see [agentskills/adding-directives/](agentskills/adding-directives/SKILL.md). ## Testing > **Every contribution that changes logic must include automated tests.** A PR without tests for > new or changed behavior will not be accepted. If testing is truly infeasible, explain why in the > PR description — but this should be exceptional. > **Unit tests are always preferred over integration tests.** Unit tests are faster, more reliable, > easier to debug, and cheaper to run on CI. Only add integration tests when the behavior cannot be > adequately verified at the unit level (e.g. end-to-end CLI invocation, launcher-specific behavior, > cross-process interactions). > **Always re-run and verify tests locally before submitting.** After any logic change, run the > relevant test suites on your machine and confirm they pass. Do not rely on CI to catch failures — > CI resources are shared, and broken PRs waste maintainer time. **Unit tests**: munit, in each module’s `test` submodule. Run commands above; add tests in `modules/build/.../tests/` or `modules/cli/src/test/scala/`. Prefer unit over integration. **Integration tests**: `modules/integration/`; they run the CLI as a subprocess. See [agentskills/integration-tests/](agentskills/integration-tests/SKILL.md) for structure and how to add tests. ## Pre-PR checklist 1. Code compiles: `./mill -i __.compile` 2. Tests added and passing locally (unit tests first, integration if needed) 3. Code formatted: `scala-cli fmt .` 4. Imports ordered: `./mill -i __.fix` 5. Reference docs regenerated (if options/directives changed): `./mill -i 'generate-reference-doc[]'.run` 6. PR template filled, LLM usage stated ## Code style Code style is enforced. **Scala 3**: Prefer `if … then … else`, `for … do`/`yield`, `enum`, `extension`, `given`/`using`, braceless blocks, top-level defs. Use union/intersection types when they simplify signatures. Always favor Scala 3 idiomatic syntax. **Functional**: Prefer `val`, immutable collections, `case class`.copy(). Prefer expressions over statements; prefer `map`/`flatMap`/`fold`/`for`-comprehensions over loops. Use `@tailrec` for tail recursion. Avoid `null`; use `Option`/ `Either`/`EitherCps` (build-macros). Keep functions small; extract helpers. **No duplication**: Extract repeated logic into shared traits or utils (`*Options` traits, companion helpers, `CommandHelpers`, `TestUtil`). Check for existing abstractions before copying. **Logging**: Use the project `Logger` only — never `System.err` or `System.out`. Logger respects verbosity (`-v`, `-q`). Use `logger.message(msg)` (default), `logger.log(msg)` (verbose), `logger.debug(msg)` (debug), `logger.error(msg)` ( always). In commands: `options.shared.logging.logger`; in build code it is passed in; in tests use `TestLogger`. **Mutability**: OK in hot paths or when a Java API requires it; keep scope minimal. ## Further reference [DEV.md](DEV.md), [CONTRIBUTING.md](CONTRIBUTING.md), [INTERNALS.md](INTERNALS.md). ================================================ FILE: CODE_OF_CONDUCT.md ================================================ Scala CLI uses the [Scala Code of Conduct](https://scala-lang.org/conduct/) for all communication and discussion. This includes both GitHub, Discord and other more direct lines of communication such as email. ================================================ FILE: CONTRIBUTING.md ================================================ # Thanks for contributing to Scala CLI! This doc is meant as a guide on how best to contribute to Scala CLI. ## Creating issues Whenever you happen upon something that needs improvement, be sure to come back to us and create an issue. Please make use of the available templates and answer all the included questions, so that the maintenance team can understand your problem easier. ## Pull requests ### Fork-Pull We accept external pull requests according to the [standard GitHub fork-pull flow](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork). Create a fork of this repository, commit your changes there and create a pull request. We try to review those as often as possible. ### Main & stable branches #### `main` All code changes should branch from [main](https://github.com/VirtusLab/scala-cli/tree/main) (which is also the default branch). #### `stable` and documentation changes However, documentation changes which don't depend on any not-yet-released code changes should branch from [stable](https://github.com/VirtusLab/scala-cli/tree/stable). This allows the CI to immediately update the website. A subsequent PR from `stable` back to `main` is created automatically. ### Rules for a well-formed PR Whenever reasonable, we try to follow the following set of rules when merging code to the repository. Following those will save you from getting a load of comments and speed up the code review. - If you are using LLM-based tools to assist you in your contribution, state that clearly in the PR description and refer to our [LLM usage policy](LLM_POLICY.md) for rules and guidelines regarding usage of LLM-based tools in contributions. - If the PR is meant to be merged as a single commit (`squash & merge`), please make sure that you modify only one thing. - This means such a PR shouldn't include code clean-up, a secondary feature or bug fix, just the single thing mentioned in the title. - If it's not obvious, please mention it in the PR description or a comment. - Otherwise, make sure you keep all the commits nice and tidy: - all side-refactors, nitpick changes, formatting fixes and other side-changes should be extracted to separate commits with the `NIT` prefix in the commit message; - similarly, code review comments regarding such changes should be marked with the same prefix; - ensure everything compiles at every commit (`./mill -i __.compile`); - ensure everything is well formatted at every commit (`scala-cli fmt .` or `scalafmt`); - ensure imports are well-ordered at every commit (`./mill -i __.fix`); - ensure reference docs are up-to date at every commit (`./mill -i 'generate-reference-doc[]'.run`); - ensure all tests pass at every commit (refer to the [dev docs](DEV.md) on how to run tests); - nobody expects you to run all the unit and integration tests for all platforms locally, that'd take too long; - just make sure the test suites relevant to your changes pass on your local machine. Other notes: - fill the pull request template; - make sure to add tests wherever possible; - favor unit tests over integration tests where applicable; - try to add scaladocs for key classes and functions; - try to add comments where your code isn't self-explanatory; - if you're changing the app behaviour or adding a new feature, make sure to add docs on the website (or note in the PR that you'll do it separately). ================================================ FILE: DEV.md ================================================ ## Developer docs ### Requirements Building Scala CLI requires JVM 17 to work properly. In theory, our build is able to download and install for its own needs JVM 17 on some OSes however it may not work in Intellij / Metals out of the box. The Scala CLI sources ship with Mill launchers, so that Mill itself doesn't need to be installed on your system. ### Common commands #### Running the CLI from sources Run the `scala` target with Mill: ```bash ./mill -i scala …arguments… ``` This is the equivalent of running the `cli` task with the default Scala version: ```bash ./mill -i 'cli[]'.run …arguments… ``` #### Debugging the CLI from sources ```bash ./mill -i debug debug-port …arguments… ``` which is short for: ```bash ./mill -i 'cli[]'.debug debug-port …arguments… ``` E.g: ```bash ./mill -i 'cli[]'.debug 5050 ~/Main.scala -S 3.3.0 ``` #### Run unit tests This command runs the unit tests from the `build-module` module. ```bash ./mill 'build-module.test' ``` If you want to run unit tests for another module, set `module_name` to the name of the module from which you want to run the unit tests: ```bash ./mill 'module_name.test' ``` To can filter unit test suites: ```bash ./mill 'build-module[].test' 'scala.build.tests.BuildTestsScalac.*' ./mill 'build-module[].test' 'scala.build.tests.BuildTestsScalac.simple' ``` #### Run integration tests with the JVM launcher ```bash ./mill integration.test.jvm ``` Filter test suites with ```bash ./mill integration.test.jvm 'scala.cli.integration.RunTestsDefault.*' ./mill integration.test.jvm 'scala.cli.integration.RunTestsDefault.Multiple scripts' ``` You can pass the `--debug` option to debug Scala CLI when running integration tests. Note that this allows to debug the Scala CLI launcher (the app) and not the integration test code itself. The debugger is being run in the `attach` mode. ```bash ./mill integration.test.jvm 'scala.cli.integration.RunTestsDefault.*' --debug ``` The debug option uses 5005 port by default. It is possible to change it as follows: ```bash ./mill integration.test.jvm 'scala.cli.integration.RunTestsDefault.*' --debug:5006 ``` #### Run integration tests with the native launcher (generating the launcher can take several minutes) ```bash ./mill integration.test.native ./mill integration.test.native 'scala.cli.integration.RunTestsDefault.*' ``` #### Generate JUnit test reports As running tests with mill generates output in a non-standard JSON format, we have a script for converting it to the more well known JUnit XML test report format which we can then process and view on the CI. In case you want to generate a test report locally, you can run the following command: ```bash .github/scripts/generate-junit-reports.sc Project Structure` that: - in `Project Settings/Project` `SDK` and `Language level` is set to **17** - in `Project Settings/Modules` all the modules have `Language level` set to **17** - in `Platform Settings/SDKs` only **Java 17** is visible Otherwise, some IDE features may not work correctly, i.e. the debugger might crash upon connection. #### Generate a native launcher ```bash ./mill -i show 'cli[]'.nativeImage ``` This prints the path to the generated native image. The file named `scala` at the root of the project should also be a link to it. (Note that the link is committed and is always there, whether the files it points at exists or not.) #### Generate a JVM launcher ```bash ./mill -i show 'cli[]'.launcher ``` This prints the path to the generated launcher. This launcher is a JAR, that directly re-uses the class directories of the modules of the project (so that cleaning up those classes will break the launcher). If this is a problem (if you wish to run the launcher on another machine or from a Docker image for example), use a native launcher (see above) or a standalone JVM one (see below). #### Generate a standalone JVM launcher ```bash ./mill -i show 'cli[]'.standaloneLauncher ``` This prints the path to the generated launcher. This launcher is a JAR, that embeds JARs of the scala-cli modules, and downloads their dependencies from Maven Central upon first launch (using the coursier cache, just like a coursier bootstrap). ### Helper projects A number of features of Scala CLI are managed from external projects, living under the [`scala-cli`](https://github.com/scala-cli) and [`VirtusLab`](https://github.com/VirtusLab) organizations on GitHub. These projects can be used by Scala CLI as libraries pulled before it's compiled, but also as binaries. In the latter case, Scala CLI downloads on-the-fly binaries from these repositories' GitHub release assets, and runs them as external processes. Here's some of the more important external projects used by Scala CLI: - [scala-js-cli-native-image](https://github.com/VirtusLab/scala-js-cli): provides a binary running the Scala.js linker - [scala-cli-signing](https://github.com/VirtusLab/scala-cli-signing): provides both libraries and binaries to handle PGP concerns in Scala CLI - [scala-packager](https://github.com/VirtusLab/scala-packager): provides a library to package applications in native formats - [libsodiumjni](https://github.com/VirtusLab/libsodiumjni): provides minimal JNI bindings for [libsodium](https://github.com/jedisct1/libsodium), that is used by Scala CLI to encrypt secrets uploaded as GitHub repository secrets in the `publish setup` sub-command - [scala-cli-setup](https://github.com/VirtusLab/scala-cli-setup): a GitHub Action to install Scala CLI. - [bloop-core](https://github.com/scala-cli/bloop-core): a fork of [bloop](https://github.com/scalacenter/bloop) stripped up of its benchmark infrastructure and build integrations. - [no-crc32-zip-input-stream](https://github.com/VirtusLab/no-crc32-zip-input-stream): A copy of `ZipInputStream` from OpenJDK, with CRC32 calculations disabled. - [lightweight-spark-distrib](https://github.com/VirtusLab/lightweight-spark-distrib): a small application allowing to make Spark distributions more lightweight. - [java-class-name](https://github.com/VirtusLab/java-class-name): a small library to extract class names from Java sources. Legacy projects: - [scalafmt-native-image](https://github.com/VirtusLab/scalafmt-native-image): GraalVM native-image launchers for `scalafmt` (used for `scalafmt` versions < 3.9.1, no longer maintained) The use of external binaries allows to make the Scala CLI binary slimmer and faster to generate, but also allow to lower memory requirements to generate it (allowing to generate these binaries on the GitHub-provided GitHub actions hosts). ### Website The Scala CLI website is built with [Docusaurus](https://v1.docusaurus.io/en/) and uses [Infima](https://infima.dev/docs/layout/spacing) for styling. Ensure you are using Node >= 16.14.2. #### Generate the website once ```bash cd website yarn yarn build npm run serve ``` #### Generate the website continuously ```bash cd website yarn yarn run start ``` ### Verifying the documentation We have a built-in tool to validate `.md` files called [Sclicheck](/sclicheck/Readme.md). All `Sclicheck` tests can be run with `Mill` + `munit`: (and this is what we run on the CI, too) ```bash ./mill -i 'docs-tests[]'.test ``` The former also includes testing gifs and `Sclicheck` itself. To just check the documents, run: ```bash ./mill -i 'docs-tests[]'.test 'sclicheck.DocTests.*' ``` You can also check all root docs, commands, reference docs, guides or cookbooks: ```bash ./mill -i 'docs-tests[]'.test 'sclicheck.DocTests.root*' ./mill -i 'docs-tests[]'.test 'sclicheck.DocTests.guide*' ./mill -i 'docs-tests[]'.test 'sclicheck.DocTests.command*' ./mill -i 'docs-tests[]'.test 'sclicheck.DocTests.cookbook*' ./mill -i 'docs-tests[]'.test 'sclicheck.DocTests.reference*' ``` Similarly, you can check single files: ```bash ./mill -i 'docs-tests[]'.test 'sclicheck.DocTests. ' ``` For example, to run the check on `compile.md` ```bash ./mill -i 'docs-tests[]'.test 'sclicheck.DocTests.command compile' ``` ## Scala CLI logos Package with various logos for scala-cli can be found on [google drive](https://drive.google.com/drive/u/1/folders/1M6JeQXmO4DTBeRBKAFJ5HH2p_hbfQnqS) ## Launcher script There is a script `scala-cli-src` in the repository root that is intended to work exactly like released scala-cli, but using a binary compiled the worktree. Just add it to your PATH to get the already-released-scala-cli experience. ## CI change detection On pull requests, the CI workflow detects which files changed and skips jobs that are not relevant. Pushes to `main`, `v*` tags, and manual dispatches always run everything. ### Override keywords You can force specific job groups to run regardless of which files changed by including these keywords anywhere in the PR body (description): | Keyword | Effect | |---------|--------| | `[test_all]` | Run **all** CI jobs, no skipping | | `[test_native]` | Force native launcher builds and native integration tests | | `[test_integration]` | Force JVM integration tests | | `[test_docs]` | Force documentation tests | | `[test_format]` | Force format and scalafix checks | For example, if your PR only touches documentation, but you want to verify native launchers still build, add `[test_native]` to the PR description. ## Releases Instructions on how to release - [Release Procedure](https://github.com/VirtusLab/scala-cli/blob/main/.github/release/release-procedure.md) ## Debugging BSP server The easiest way to debug BSP sever is using `scala-cli-src` script with `--bsp-debug-port 5050` flag (the port should be unique to the workspace where BSP will be debugged). In such case BSP will be launched using local source and will run on JVM. It will also expects a debugger running in the listen mode using provided port (so the initialization of the connection can be debugged). In such case we recommend to have option to auto rerun debugging session off (so there is always a debugger instance ready to be used). ## GraalVM reflection configuration As Scala CLI is using GraalVM native image, it requires a configuration file for reflection. The configuration for the `cli` module is located in [the reflect-config.json](modules/cli/src/main/resources/META-INF/native-image/org.virtuslab/scala-cli-core/reflect-config.json) file. When adding new functionalities or updating dependencies, it might turn out the reflection configuration for some class may be missing. The relevant error message when running `integration.test.native` may be misleading, usually with a `ClassNotFoundException` or even with a functionality seemingly being skipped. This is because logic referring to classes with missing reflection configuration may be skipped for the used native image. To generate the relevant configuration automatically, you can run: ```bash ./mill -i 'cli[]'.runWithAssistedConfig ``` Just make sure to run it exactly the same as the native image would have been run, as the configuration is generated for a particular invocation path. The run has to succeed as well, as the configuration will only be fully generated after an exit code 0. ```text Config generated in out/cli//runWithAssistedConfig.dest/config ``` As a result, you should get the path to the generated configuration file. It might contain some unnecessary entries, so make sure to only copy what you truly need. As the formatting of the `reflect-config.json` is verified on the CI, make sure to run the following command to adjust it accordingly before committing: ```bash ./mill -i __.formatNativeImageConf ``` For more info about reflection configuration in GraalVM, check [the relevant GraalVM Reflection docs](https://www.graalvm.org/latest/reference-manual/native-image/dynamic-features/Reflection/). ## Overriding Scala versions in Scala CLI builds It's possible to override the internal Scala version used to build Scala CLI, as well as the default version used by the CLI itself with Java props. - `scala.version.internal` - overrides the internal Scala version used to build Scala CLI - `scala.version.user` - overrides the default Scala version used by the CLI itself NOTE: remember to run `./mill clean` to make sure the Scala versions aren't being cached anywhere. ```bash ./mill -i clean ./mill -i --define scala.version.internal=3.4.0-RC1-bin-20231012-242ba21-NIGHTLY --define scala.version.user=3.4.0-RC1-bin-20231012-242ba21-NIGHTLY scala version --offline # Scala CLI version: 1.x.x-SNAPSHOT # Scala version (default): 3.4.0-RC1-bin-20231012-242ba21-NIGHTLY ``` ================================================ FILE: Dockerfile ================================================ FROM eclipse-temurin:17 as build RUN apt update && apt install build-essential libz-dev clang procps git -y WORKDIR /workdir COPY . . RUN ./mill -i copyTo --task 'cli[].base-image.nativeImage' --dest "./docker-out/scala-cli" 1>&2 FROM debian:stable-slim RUN apt update && apt install build-essential libz-dev clang procps -y COPY --from=build /workdir/docker-out/scala-cli /usr/bin/scala-cli RUN \ echo "println(1)" | scala-cli -S 3 - -v -v -v && \ echo "println(1)" | scala-cli -S 2.13 - -v -v -v && \ echo "println(1)" | scala-cli -S 2.12 - -v -v -v RUN \ echo "println(1)" | scala-cli --power package --native _.sc --force && \ echo "println(1)" | scala-cli --power package --native-image _.sc --force ENTRYPOINT ["scala-cli"] ================================================ FILE: INTERNALS.md ================================================ # Internals overview ## Modules Modules live under `modules/`. Each sub-directory there has a corresponding mill module definition in `build.sc` (but for `integration`). Most of the code currently lives in the `build` module. The `cli` module depends on `build`, gets packaged as a native-image executable, and distributed as `scala-cli` binary. The other modules are either: - integration tests - utility modules, that `build` either: - depends on - fetches at run-time. ## Utility modules These are: - `runner`: simple app that starts a main class, catches any exception it throws and pretty-prints it. - `test-runner`: finds test frameworks, test suites, and runs them - `tasty-lib`: edits file names in `.tasty` files ## Tests The tests live either in: - `build`: unit tests - `integration`: integration tests Run unit tests with ```bash ./mill 'build[_].test' ``` Run integration tests with a JVM-based `scala-cli` with ```bash ./mill integration.test.jvm ``` Run integration tests with a native-image-based `scala-cli` with ```bash ./mill integration.test.native ``` ## General workflow in most `scala-cli` commands We roughly go from user inputs to byte code through 3 classes: - `Inputs`: ADT for input files / directories. - `Sources`: processed sources, ready to be passed to scalac - `Build`: compilation result: success or failure. Most commands - take the arguments passed on the command-line: we have an `Array[String]` - check whether each of them is a `.scala` file, an `.sc` file, a directory, …: we get an `Inputs` instance - reads the directories, the `.scala` / `.sc` files: we get a `Sources` instance - compile those sources: we get a `Build` instance - do something with the build output (run it, run tests, package it, …) In watch mode, we loop over the last 3 steps (`Inputs` is computed only once, the rest is re-computed upon file change). ## Source pre-processing Some input files cannot be passed as is to scalac, if they are scripts (`.sc` files), which contain top-level statements Scripts get wrapped. If the script `a/b/foo.sc` contains ```scala val n = 2 ``` we compile it as ```scala package a.b object foo { val n = 2 def main(args: Array[String]): Unit = () } ``` Basically, - its directory dictates its package - we put its sources as is in an object - we add a `main` method ## Build outputs post-processing The source generation changes: - file names, which now correspond to the directory where we write generated sources - positions, when we wrap code (for `.sc` files) As a consequence, some build outputs contains wrong paths or positions: - diagnostics (warning and error messages) contain file paths and positions, used in reporting - byte code contains file names and line numbers, that are used in stack traces - semantic DBs contain relative file paths and positions, used by IDEs - TASTy files contain relative file paths, used in pretty stack traces We post-process those build outputs, to adjust positions and file paths of the generated sources: various "mappings" are computed out of the generated sources list, and are used to adjust: - diagnostics: done in memory, right before printing diagnostics - byte code: done using the ASM library - semantic DBs: we parse the semantic DBs, edit them in memory, and write them back on disk - TASTy files: we partly parse them in memory, edit names that contain source file paths, and write them back on disk ## Publishing scalajs-cli ### Maven Publishing - Version Synchronization: `scalajs-cli` will be published with the same version as Scala.js version, for example `1.13.0`. - Updates & Fixes: For any subsequent fixes or patches in `scalajs-cli`, we will append a numeric value to the end, like `1.13.0.1`. - GitHub Uploads - Native Launchers: With the patch release of `scalajs-cli`, native launchers are automatically uploaded to both versions, for example `1.13.0.1` and `1.13.0` tags on GitHub. - For instance: For release `1.13.0.2`, the launchers are uploaded to tags `1.13.0.2` and `1.13.0`. - ScalaCli dependency to `scalajs-cli`: - For Coursier to retrieve the most recent scalajs-cli for a specific Scala.js version, the version is set as `org.virtuslab:scalajscli_2.13:{Scala.js version}+`. For example `org.virtuslab:scalajscli_2.13:1.13.0+`. - Native Version Download: - The native version is downloaded from the Scala.js version tag. If there are updates or fixes to the native `scalajs-cli` launchers, the updated launchers are uploaded to the `1.13.0` tag during the `1.13.0.1` publishing. ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. 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. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor 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 Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You 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 the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You 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 such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its 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. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: LLM_POLICY.md ================================================ # Policy regarding LLM-generated code in contributions to Scala CLI Scala CLI accepts contributions containing code produced with AI assistance. This means that using LLM-based tooling aiding software development (like Cursor, Claude Code, Copilot or whatever else) is allowed. All such contributions are regulated by the policy defined in the Scala 3 compiler repository, which can be found at: https://github.com/scala/scala3/blob/main/LLM_POLICY.md ================================================ FILE: README.md ================================================ # scala-cli [![Build status](https://github.com/VirtusLab/scala-cli/workflows/CI/badge.svg)](https://github.com/VirtusLab/scala-ci/actions?query=workflow%3ACI) [![Maven Central](https://img.shields.io/maven-central/v/org.virtuslab.scala-cli/cli_3.svg)](https://maven-badges.herokuapp.com/maven-central/org.virtuslab.scala-cli/cli_3) [![Discord](https://img.shields.io/discord/632277896739946517.svg?label=&logo=discord&logoColor=ffffff&color=404244&labelColor=6A7EC2)](https://discord.gg/KzQdYkZZza) Scala CLI is a command-line tool to interact with the Scala language. It lets you compile, run, test, and package your Scala code. (and more!) It shares some similarities with build tools, but it doesn't aim at supporting multi-module projects, nor to be extended via a task system. As of Scala 3.5.0, Scala CLI has become the official `scala` runner of the language (for more information refer to [SIP-46](https://github.com/scala/improvement-proposals/pull/46)). For more details on using Scala CLI via the `scala` command, refer to [this doc](https://scala-cli.virtuslab.org/docs/reference/scala-command/). ## Docs - user-facing documentation: [scala-cli.virtuslab.org](https://scala-cli.virtuslab.org/) - [contributing guide](CONTRIBUTING.md) - [developer docs](DEV.md) - [app internals](INTERNALS.md) - [docs website readme](website/README.md) - [docs gifs readme](gifs/README.md) - [sclicheck readme](modules/docs-tests/README.md) - [gcbenchmark readme](gcbenchmark/README.md) - [release procedure](.github/release/release-procedure.md) - [code of conduct](CODE_OF_CONDUCT.md) ================================================ FILE: agentskills/README.md ================================================ # Agent skills (Scala CLI) This directory holds **agent skills** — task-specific guidance loaded on demand by AI coding agents. The layout is tool-agnostic; Cursor, Claude Code, Codex, and other tools that support a standard skill directory can use this (e.g. by configuring or symlinking to `.agents/skills/` if required). Each subdirectory contains a `SKILL.md` with frontmatter and instructions. See [agentskills/agentskills](https://github.com/agentskills/agentskills) for the open standard. ================================================ FILE: agentskills/adding-directives/SKILL.md ================================================ --- name: scala-cli-adding-directives description: Add or change using directives in Scala CLI. Use when adding a new //> using directive, registering a directive handler, or editing directive preprocessing. --- # Adding a new directive (Scala CLI) 1. **Create a case class** in `modules/directives/src/main/scala/scala/build/preprocessing/directives/` extending one of: - `HasBuildOptions` — produces `BuildOptions` directly - `HasBuildOptionsWithRequirements` — produces `BuildOptions` with scoped requirements (e.g. `test.dep`) - `HasBuildRequirements` — produces `BuildRequirements` (for `//> require`) 2. **Annotate**: `@DirectiveLevel(SpecificationLevel.EXPERIMENTAL)`, `@DirectiveDescription("…")`, `@DirectiveUsage("…")`, `@DirectiveExamples("…")`, `@DirectiveName("key")` on fields. 3. **Companion**: `val handler: DirectiveHandler[YourDirective] = DirectiveHandler.derive` 4. **Register** in `modules/build/.../DirectivesPreprocessingUtils.scala` in the right list: `usingDirectiveHandlers`, `usingDirectiveWithReqsHandlers`, or `requireDirectiveHandlers`. 5. **Regenerate reference docs**: `./mill -i 'generate-reference-doc[]'.run` CLI options always override directive values when both set the same thing. ================================================ FILE: agentskills/integration-tests/SKILL.md ================================================ --- name: scala-cli-integration-tests description: Add or run Scala CLI integration tests. Use when adding integration tests, debugging RunTests/CompileTests/etc., or working in modules/integration. --- # Integration tests (Scala CLI) **Location**: `modules/integration/`. Tests invoke the CLI as an external process. **Run**: `./mill -i integration.test.jvm` (all). Filter: `./mill -i integration.test.jvm 'scala.cli.integration.RunTestsDefault.*'` or by test name. Native: `./mill -i integration.test.native`. **Structure**: `*TestDefinitions.scala` (abstract, holds test logic) → `*TestsDefault`, `*Tests213`, etc. (concrete, Scala version trait). Traits: `TestDefault`, `Test212`, `Test213`, `Test3Lts`, `Test3NextRc`. **Adding a test**: 1. Open the right `*TestDefinitions` (e.g. `RunTestDefinitions` for `run`). 2. Add `test("description") { … }` using `TestInputs(os.rel / "Main.scala" -> "…").fromRoot { root => … }` and `os.proc(TestUtil.cli, "run", …).call(cwd = root)`. 3. Assert on stdout/stderr. **Helpers**: `TestInputs(...).fromRoot`, `TestUtil.cli`. Test groups (CI): `SCALA_CLI_IT_GROUP=1..5`; see `modules/integration/` for group mapping. ================================================ FILE: build.mill ================================================ //| mill-jvm-version: system|17 //| mvnDeps: //| - io.github.alexarchambault.mill::mill-native-image::0.2.4 //| - io.github.alexarchambault.mill::mill-native-image-upload:0.2.4 //| - com.goyeau::mill-scalafix::0.6.0 //| - com.lumidion::sonatype-central-client-requests:0.6.0 //| - io.get-coursier:coursier-launcher_2.13:2.1.25-M24 //| - org.eclipse.jgit:org.eclipse.jgit:7.5.0.202512021534-r package build import build.ci.publishVersion import build.project.deps import deps.{Cli, Deps, Docker, Java, Scala, TestDeps} import build.project.publish import publish.{ScalaCliPublishModule, finalPublishVersion, ghName, ghOrg, organization} import build.project.settings import settings.{ CliLaunchers, FormatNativeImageConf, HasTests, LocalRepo, LocatedInModules, PublishLocalNoFluff, ScalaCliCrossSbtModule, ScalaCliScalafixModule, isCI, jvmPropertiesFileName, localRepoResourcePath, platformExecutableJarExtension, projectFileName, workspaceDirName } import deps.customRepositories import deps.alpineVersion import build.project.website import coursier.Repository import java.io.File import java.net.URL import java.nio.charset.Charset import java.util.Locale import io.github.alexarchambault.millnativeimage.upload.Upload import mill.* import mill.api.{BuildCtx, BuildInfo, ModuleCtx, Task} import mill.scalalib.* import scalalib.{publish as _, *} import mill.javalib.testrunner.TestResult import mill.util.{Tasks, VcsVersion} import _root_.scala.util.{Properties, Using} import _root_.scala.util.{Properties, Using} object cli extends Cross[Cli](Scala.scala3MainVersions) with CrossScalaDefaultToInternal trait CrossScalaDefault { self: Cross[?] => def crossScalaDefaultVersion: String override def defaultCrossSegments: Seq[String] = Seq(crossScalaDefaultVersion) } trait CrossScalaDefaultToInternal extends CrossScalaDefault { self: Cross[?] => override def crossScalaDefaultVersion: String = Scala.defaultInternal } trait CrossScalaDefaultToRunner extends CrossScalaDefault { self: Cross[?] => override def crossScalaDefaultVersion: String = Scala.runnerScala3 } // Publish a bootstrapped, executable jar for a restricted environments object cliBootstrapped extends ScalaCliPublishModule { override def unmanagedClasspath: T[Seq[PathRef]] = Task(cli(Scala.defaultInternal).nativeImageClassPath().filter(ref => os.exists(ref.path))) override def jar: T[PathRef] = assembly() import mill.scalalib.Assembly override def prependShellScript: T[String] = Task("") override def mainClass: T[Option[String]] = Some("scala.cli.ScalaCli") override def assemblyRules: Seq[Assembly.Rule] = Seq( Assembly.Rule.ExcludePattern(".*\\.tasty"), Assembly.Rule.ExcludePattern(".*\\.semanticdb") ) ++ super.assemblyRules override def resources: T[Seq[PathRef]] = super.resources() ++ Seq(propertiesFilesResources()) def propertiesFilesResources: T[PathRef] = Task(persistent = true) { val dir = Task.dest / "resources" val dest = dir / "java-properties" / "scala-cli-properties" val content = "scala-cli.kind=jvm.bootstrapped" os.write.over(dest, content, createFolders = true) PathRef(dir) } } object `specification-level` extends Cross[SpecificationLevel](Scala.scala3MainVersions) with CrossScalaDefaultToInternal object `build-macros` extends Cross[BuildMacros](Scala.scala3MainVersions) with CrossScalaDefaultToInternal object config extends Cross[Config](Scala.scala3MainVersions) with CrossScalaDefaultToInternal object options extends Cross[Options](Scala.scala3MainVersions) with CrossScalaDefaultToInternal object directives extends Cross[Directives](Scala.scala3MainVersions) with CrossScalaDefaultToInternal object core extends Cross[Core](Scala.scala3MainVersions) with CrossScalaDefaultToInternal object `build-module` extends Cross[Build](Scala.scala3MainVersions) with CrossScalaDefaultToInternal object runner extends Cross[Runner](Scala.runnerScalaVersions) with CrossScalaDefaultToRunner object `test-runner` extends Cross[TestRunner](Scala.runnerScalaVersions) with CrossScalaDefaultToRunner object `java-test-runner` extends JavaTestRunner with LocatedInModules object `tasty-lib` extends Cross[TastyLib](Scala.scala3MainVersions) with CrossScalaDefaultToInternal object `scala-cli-bsp` extends JavaModule with ScalaCliPublishModule with LocatedInModules { override def mvnDeps: T[Seq[Dep]] = super.mvnDeps() ++ Seq( Deps.bsp4j ) } object integration extends CliIntegration { object test extends IntegrationScalaTests { override def testParallelism: T[Boolean] = !isCI override def testForkGrouping: T[Seq[Seq[String]]] = if isCI then discoveredTestClasses().grouped(1).toSeq else super.testForkGrouping() override def mvnDeps: T[Seq[Dep]] = super.mvnDeps() ++ Seq( Deps.coursierArchiveCache, Deps.jgit, Deps.jsoup ) } object docker extends CliIntegrationDocker { object test extends ScalaCliTests { override def sources: T[Seq[PathRef]] = super.sources() ++ integration.sources() def tmpDirBase: T[PathRef] = Task(persistent = true) { PathRef(Task.dest / "working-dir") } override def forkEnv: T[Map[String, String]] = super.forkEnv() ++ Seq( "SCALA_CLI_TMP" -> tmpDirBase().path.toString, "SCALA_CLI_IMAGE" -> "scala-cli", "SCALA_CLI_PRINT_STACK_TRACES" -> "1" ) } } object `docker-slim` extends CliIntegrationDocker { object test extends ScalaCliTests { override def sources: T[Seq[PathRef]] = integration.docker.test.sources() def tmpDirBase: T[PathRef] = Task(persistent = true) { PathRef(Task.dest / "working-dir") } override def forkEnv: T[Map[String, String]] = super.forkEnv() ++ Seq( "SCALA_CLI_TMP" -> tmpDirBase().path.toString, "SCALA_CLI_IMAGE" -> "scala-cli-slim", "SCALA_CLI_PRINT_STACK_TRACES" -> "1" ) } } } object `docs-tests` extends Cross[DocsTests](Scala.scala3MainVersions) with CrossScalaDefaultToInternal trait DocsTests extends CrossSbtModule with ScalaCliScalafixModule with LocatedInModules with HasTests { main => override def mvnDeps: T[Seq[Dep]] = Seq( Deps.fansi, Deps.osLib, Deps.pprint ) def tmpDirBase: T[PathRef] = Task(persistent = true) { PathRef(Task.dest / "working-dir") } def extraEnv: T[Seq[(String, String)]] = Task { Seq( "SCLICHECK_SCALA_CLI" -> cli(crossScalaVersion).standaloneLauncher().path.toString, "SCALA_CLI_CONFIG" -> (tmpDirBase().path / "config" / "config.json").toString ) } override def forkEnv: T[Map[String, String]] = super.forkEnv() ++ extraEnv() def constantsFile: T[PathRef] = Task(persistent = true) { val dir = Task.dest / "constants" val dest = dir / "Constants.scala" val code = s"""package sclicheck | |/** Build-time constants. Generated by mill. */ |object Constants { | def coursierOrg = "${Deps.coursier.dep.module.organization.value}" | def coursierCliModule = "${Deps.coursierCli.dep.module.name.value}" | def coursierCliVersion = "${Deps.Versions.coursierCli}" | def defaultScalaVersion = "${Scala.defaultUser}" | def scalaLegacyRunnerVersion = "${Scala.scalaLegacyRunnerVersion}" | def alpineVersion = "$alpineVersion" |} |""".stripMargin if (!os.isFile(dest) || os.read(dest) != code) os.write.over(dest, code, createFolders = true) PathRef(dir) } override def generatedSources: T[Seq[PathRef]] = super.generatedSources() ++ Seq(constantsFile()) object test extends ScalaCliTests with ScalaCliScalafixModule { override def testParallelism: T[Boolean] = !isCI override def testForkGrouping: T[Seq[Seq[String]]] = if isCI then discoveredTestClasses().grouped(1).toSeq else super.testForkGrouping() override def forkEnv: T[Map[String, String]] = super.forkEnv() ++ extraEnv() ++ Seq( "SCALA_CLI_EXAMPLES" -> (BuildCtx.workspaceRoot / "examples").toString, "SCALA_CLI_GIF_SCENARIOS" -> (BuildCtx.workspaceRoot / "gifs" / "scenarios").toString, "SCALA_CLI_WEBSITE_IMG" -> (BuildCtx.workspaceRoot / "website" / "static" / "img").toString, "SCALA_CLI_GIF_RENDERER_DOCKER_DIR" -> (BuildCtx.workspaceRoot / "gifs").toString, "SCALA_CLI_SVG_RENDERER_DOCKER_DIR" -> (BuildCtx.workspaceRoot / "gifs" / "svg_render").toString ) private def customResources: T[Seq[PathRef]] = { val customPaths: Seq[os.Path] = Seq( BuildCtx.workspaceRoot / "website" / "docs" / "commands", BuildCtx.workspaceRoot / "website" / "docs" / "cookbooks" ) Task.Sources(customPaths*) } override def resources: T[Seq[PathRef]] = // Adding markdown directories here, so that they're watched for changes in watch mode super.resources() ++ customResources() } } object packager extends ScalaModule { override def scalaVersion: T[String] = Scala.scala3Lts override def mvnDeps: T[Seq[Dep]] = Seq( Deps.scalaPackagerCli ) override def mainClass: T[Option[String]] = Some("packager.cli.PackagerCli") } object `generate-reference-doc` extends Cross[GenerateReferenceDoc](Scala.scala3MainVersions) with CrossScalaDefaultToInternal trait GenerateReferenceDoc extends CrossSbtModule with LocatedInModules with ScalaCliScalafixModule { override def moduleDeps: Seq[JavaModule] = Seq( cli(crossScalaVersion) ) override def repositoriesTask: Task[Seq[Repository]] = Task.Anon(super.repositoriesTask() ++ customRepositories) override def mvnDeps: T[Seq[Dep]] = Seq( Deps.argonautShapeless, Deps.caseApp, Deps.munit ) override def mainClass: T[Option[String]] = Some("scala.cli.doc.GenerateReferenceDoc") override def forkEnv: T[Map[String, String]] = super.forkEnv() ++ Seq( "SCALA_CLI_POWER" -> "true" ) } object dummy extends LocatedInModules { // dummy projects to get scala steward updates for Ammonite and scalafmt, whose // versions are used in the fmt and repl commands, and ensure Ammonite is available // for all Scala versions we support. object amm extends Cross[Amm](Scala.listMaxAmmoniteScalaVersion) trait Amm extends Cross.Module[String] with CrossScalaModule { override def crossScalaVersion: String = crossValue override def mvnDeps: T[Seq[Dep]] = { val ammoniteDep = if (crossValue == Scala.scala3Lts) Deps.ammoniteForScala3Lts else Deps.ammonite Seq(ammoniteDep) } } object scalafmt extends ScalaModule { override def scalaVersion: T[String] = Scala.defaultInternal override def mvnDeps: T[Seq[Dep]] = Seq( Deps.scalafmtCli ) } object pythonInterface extends JavaModule { override def mvnDeps: T[Seq[Dep]] = Seq( Deps.pythonInterface ) } object scalaPy extends ScalaModule { override def scalaVersion: T[String] = Scala.defaultInternal override def mvnDeps: T[Seq[Dep]] = Seq( Deps.scalaPy ) } object scalafix extends ScalaModule { override def scalaVersion: T[String] = Scala.defaultInternal override def mvnDeps: T[Seq[Dep]] = Seq( Deps.scalafixInterfaces ) } } trait BuildMacros extends ScalaCliCrossSbtModule with ScalaCliPublishModule with ScalaCliScalafixModule with HasTests with LocatedInModules { override def crossScalaVersion: String = crossValue override def compileMvnDeps: T[Seq[Dep]] = Task { if (crossScalaVersion.startsWith("3")) super.compileMvnDeps() else super.compileMvnDeps() ++ Seq(Deps.scalaReflect(crossScalaVersion)) } object test extends ScalaCliTests with ScalaCliScalafixModule { override def scalacOptions: T[Seq[String]] = Task { super.scalacOptions() ++ Seq("-deprecation") } def testNegativeCompilation(): Command[Unit] = Task.Command(exclusive = true) { val base = BuildCtx.workspaceRoot / "modules" / "build-macros" / "src" val negativeTests = Seq( "MismatchedLeft.scala" -> Seq( "Found: +EE1".r, "Found: +EE2".r, "Required: +E2".r ) ) val cpsSource = base / "main" / "scala" / "scala" / "build" / "EitherCps.scala" val cpsSourceExists = os.exists(cpsSource) if (!cpsSourceExists) System.err.println(s"Expected source file $cpsSource does not exist") else System.err.println(s"Found source file $cpsSource") assert(cpsSourceExists) val sv = scalaVersion() def compile(extraSources: os.Path*): os.CommandResult = os.proc( "scala-cli", "--cli-default-scala-version", sv, "compile", cpsSource, extraSources ).call( check = false, mergeErrIntoOut = true, cwd = BuildCtx.workspaceRoot ) val compileResult = compile() if (compileResult.exitCode != 0) { System.err.println(s"Compilation failed: $cpsSource") System.err.println(compileResult.out.text()) } else System.err.println(s"Compiled $cpsSource successfully") assert(0 == compileResult.exitCode) val notPassed = negativeTests.filter { case (testName, expectedErrors) => val testFile = base / "negative-tests" / testName val res = compile(testFile) println(s"Compiling $testName:") println(res.out.text()) val name = testFile.last if (res.exitCode != 0) { println(s"Test case $name failed to compile as expected") val lines = res.out.lines() println(lines) expectedErrors.forall { expected => if (lines.exists(expected.findFirstIn(_).nonEmpty)) false else { println(s"ERROR: regex `$expected` not found in compilation output for $testName") true } } } else { println(s"[ERROR] $name compiled successfully but it should not!") true } } assert(notPassed.isEmpty) } } } def asyncScalacOptions(scalaVersion: String) = if (scalaVersion.startsWith("3")) Nil else Seq("-Xasync") trait ProtoBuildModule extends ScalaCliPublishModule with HasTests with ScalaCliScalafixModule trait Core extends ScalaCliCrossSbtModule with ScalaCliPublishModule with HasTests with ScalaCliScalafixModule with LocatedInModules { override def crossScalaVersion: String = crossValue override def moduleDeps: Seq[SonatypeCentralPublishModule] = Seq( config(crossScalaVersion) ) override def compileModuleDeps: Seq[JavaModule] = Seq( `build-macros`(crossScalaVersion) ) override def scalacOptions: T[Seq[String]] = Task { super.scalacOptions() ++ asyncScalacOptions(crossScalaVersion) } override def repositoriesTask: Task[Seq[Repository]] = Task.Anon(super.repositoriesTask() ++ deps.customRepositories) override def mvnDeps: T[Seq[Dep]] = super.mvnDeps() ++ Seq( Deps.bloopRifle.exclude(("org.scala-lang.modules", "scala-collection-compat_2.13")), Deps.collectionCompat, Deps.coursierJvm // scalaJsEnvNodeJs brings a guava version that conflicts with this .exclude(("com.google.collections", "google-collections")) // Coursier is not cross-compiled and pulls jsoniter-scala-macros in 2.13 .exclude(("com.github.plokhotnyuk.jsoniter-scala", "jsoniter-scala-macros")) // Let's favor our config module rather than the one coursier pulls .exclude((organization, "config_2.13")) .exclude((organization, "config_3")) .exclude(("org.scala-lang.modules", "scala-collection-compat_2.13")), Deps.dependency, Deps.guava, // for coursierJvm / scalaJsEnvNodeJs, see above Deps.jgit, Deps.nativeTools, // Used only for discovery methods. For linking, look for scala-native-cli Deps.osLib, Deps.pprint, Deps.scalaJsEnvJsdomNodejs, Deps.scalaJsLogging, Deps.swoval ) override def compileMvnDeps: T[Seq[Dep]] = super.compileMvnDeps() ++ Seq( Deps.jsoniterMacros ) private def vcsState: T[String] = Task(persistent = true) { val isCI = System.getenv("CI") != null val state = VcsVersion.vcsState().format() if (isCI) state else state + "-maybe-stale" } def constantsFile: T[PathRef] = Task(persistent = true) { val dir = Task.dest / "constants" val dest = dir / "Constants.scala" val testRunnerMainClass = `test-runner`(crossScalaVersion) .mainClass() .getOrElse(sys.error("No main class defined for test-runner")) val runnerMainClass = build.runner(crossScalaVersion) .mainClass() .getOrElse(sys.error("No main class defined for runner")) val javaTestRunnerMainClass = `java-test-runner` .mainClass() .getOrElse(sys.error("No main class defined for java-test-runner")) val detailedVersionValue = if (`local-repo`.developingOnStubModules) s"""Some("${vcsState()}")""" else "None" val testRunnerOrganization = `test-runner`(crossScalaVersion) .pomSettings() .organization val javaTestRunnerOrganization = `java-test-runner` .pomSettings() .organization val code = s"""package scala.build.internal | |/** Build-time constants. Generated by mill. */ |object Constants { | def version = "${publishVersion()}" | def detailedVersion: Option[String] = $detailedVersionValue | def ghOrg = "$ghOrg" | def ghName = "$ghName" | | def scalaJsVersion = "${Scala.scalaJs}" | def scalaJsCliVersion = "${Scala.scalaJsCli}" | def scalajsEnvJsdomNodejsVersion = "${Deps.scalaJsEnvJsdomNodejs.dep.versionConstraint.asString}" | def scalaNativeVersion04 = "${Deps.Versions.scalaNative04}" | def scalaNativeVersion = "${Deps.Versions.scalaNative}" | | def testRunnerOrganization = "$testRunnerOrganization" | def testRunnerModuleName = "${`test-runner`(crossScalaVersion).artifactName()}" | def testRunnerVersion = "${`test-runner`(crossScalaVersion).publishVersion()}" | def testRunnerMainClass = "$testRunnerMainClass" | | def javaTestRunnerOrganization = "$javaTestRunnerOrganization" | def javaTestRunnerModuleName = "${`java-test-runner`.artifactName()}" | def javaTestRunnerVersion = "${`java-test-runner`.publishVersion()}" | def javaTestRunnerMainClass = "$javaTestRunnerMainClass" | | def runnerOrganization = "${build.runner(crossScalaVersion).pomSettings().organization}" | def runnerModuleName = "${build.runner(crossScalaVersion).artifactName()}" | def runnerVersion = "${build.runner(crossScalaVersion).publishVersion()}" | def runnerScala30LegacyVersion = "${Cli.runnerScala30LegacyVersion}" | def runnerScala2LegacyVersion = "${Cli.runnerScala2LegacyVersion}" | def runnerMainClass = "$runnerMainClass" | | def semanticDbPluginOrganization = "${Deps.semanticDbScalac.dep.module.organization .value}" | def semanticDbPluginModuleName = "${Deps.semanticDbScalac.dep.module.name.value}" | def semanticDbPluginVersion = "${Deps.semanticDbScalac.dep.versionConstraint.asString}" | | def semanticDbJavacPluginOrganization = "${Deps.semanticDbJavac.dep.module.organization .value}" | def semanticDbJavacPluginModuleName = "${Deps.semanticDbJavac.dep.module.name.value}" | def semanticDbJavacPluginVersion = "${Deps.semanticDbJavac.dep.versionConstraint.asString}" | | def localRepoResourcePath = "$localRepoResourcePath" | | def jmhVersion = "${Deps.Versions.jmh}" | def jmhOrg = "${Deps.jmhCore.dep.module.organization.value}" | def jmhCoreModule = "${Deps.jmhCore.dep.module.name.value}" | def jmhGeneratorBytecodeModule = "${Deps.jmhGeneratorBytecode.dep.module.name.value}" | | def ammoniteVersion = "${Deps.Versions.ammonite}" | def ammoniteVersionForScala3Lts = "${Deps.Versions.ammoniteForScala3Lts}" | def millVersion = "${BuildInfo.millVersion}" | def maxScalaNativeForMillExport = "${Deps.Versions.maxScalaNativeForMillExport}" | | def scalafmtOrganization = "${Deps.scalafmtCli.dep.module.organization.value}" | def scalafmtName = "${Deps.scalafmtCli.dep.module.name.value}" | def defaultScalafmtVersion = "${Deps.scalafmtCli.dep.versionConstraint.asString}" | | def toolkitOrganization = "${Deps.toolkit.dep.module.organization.value}" | def toolkitName = "${Deps.toolkit.dep.module.name.value}" | def toolkitTestName = "${Deps.toolkitTest.dep.module.name.value}" | def toolkitDefaultVersion = "${Deps.toolkitVersion}" | def toolkitMaxScalaNative = "${Deps.Versions.maxScalaNativeForToolkit}" | def toolkitVersionForNative04 = "${Deps.toolkitVersionForNative04}" | def toolkitVersionForNative05 = "${Deps.toolkitVersionForNative05}" | | def typelevelOrganization = "${Deps.typelevelToolkit.dep.module.organization.value}" | def typelevelToolkitDefaultVersion = "${Deps.typelevelToolkitVersion}" | def typelevelToolkitMaxScalaNative = "${Deps.Versions.maxScalaNativeForTypelevelToolkit}" | | def minimumLauncherJavaVersion = ${Java.minimumJavaLauncherJava} | def minimumBloopJavaVersion = ${Java.minimumBloopJava} | def minimumInternalJavaVersion = ${Java.minimumInternalJava} | def defaultJavaVersion = ${Java.defaultJava} | def mainJavaVersions = Seq(${Java.mainJavaVersions.sorted.mkString(", ")}) | | def defaultScalaVersion = "${Scala.defaultUser}" | def defaultScala212Version = "${Scala.scala212}" | def defaultScala213Version = "${Scala.scala213}" | def scala3NextRcVersion = "${Scala.scala3NextRc}" | def scala3NextPrefix = "${Scala.scala3NextPrefix}" | def scala3LtsPrefix = "${Scala.scala3LtsPrefix}" | def scala3Lts = "${Scala.scala3Lts}" | | def scala38Versions = Seq(${Scala.scala38Versions .sorted .map(s => s"\"$s\"") .mkString(", ")}) | def scala38MinJavaVersion = ${Java.minimumScala38Java} | | def workspaceDirName = "$workspaceDirName" | def projectFileName = "$projectFileName" | def jvmPropertiesFileName = "$jvmPropertiesFileName" | def scalacArgumentsFileName = "scalac.args.txt" | def maxScalacArgumentsCount = 5000 | | def defaultGraalVMJavaVersion = ${deps.graalVmJavaVersion} | def defaultGraalVMVersion = "${deps.graalVmCommunityVersion}" | | def scalaCliSigningOrganization = "${Deps.signingCli.dep.module.organization.value}" | def scalaCliSigningName = "${Deps.signingCli.dep.module.name.value}" | def scalaCliSigningVersion = "${Deps.signingCli.dep.versionConstraint.asString}" | def javaClassNameOrganization = "${Deps.javaClassName.dep.module.organization.value}" | def javaClassNameName = "${Deps.javaClassName.dep.module.name.value}" | def javaClassNameVersion = "${Deps.javaClassName.dep.versionConstraint.asString}" | | def signingCliJvmVersion = ${Deps.Versions.signingCliJvmVersion} | | def libsodiumVersion = "${deps.libsodiumVersion}" | def libsodiumjniVersion = "${Deps.libsodiumjni.dep.versionConstraint.asString}" | def alpineLibsodiumVersion = "${deps.alpineLibsodiumVersion}" | def condaLibsodiumVersion = "${deps.condaLibsodiumVersion}" | | def scalaPyVersion = "${Deps.scalaPy.dep.versionConstraint.asString}" | def scalaPyMaxScalaNative = "${Deps.Versions.maxScalaNativeForScalaPy}" | | def giter8Organization = "${Deps.giter8.dep.module.organization.value}" | def giter8Name = "${Deps.giter8.dep.module.name.value}" | def giter8Version = "${Deps.giter8.dep.versionConstraint.asString}" | | def sbtVersion = "${Deps.Versions.sbtVersion}" | | def mavenVersion = "${Deps.Versions.mavenVersion}" | def mavenScalaCompilerPluginVersion = "${Deps.Versions.mavenScalaCompilerPluginVersion}" | def mavenExecPluginVersion = "${Deps.Versions.mavenExecPluginVersion}" | def mavenAppArtifactId = "${Deps.Versions.mavenAppArtifactId}" | def mavenAppGroupId = "${Deps.Versions.mavenAppGroupId}" | def mavenAppVersion = "${Deps.Versions.mavenAppVersion}" | | def scalafixVersion = "${Deps.Versions.scalafix}" | | def alpineVersion = "$alpineVersion" |} |""".stripMargin if (!os.isFile(dest) || os.read(dest) != code) os.write.over(dest, code, createFolders = true) PathRef(dir) } override def generatedSources: T[Seq[PathRef]] = super.generatedSources() ++ Seq(constantsFile()) } trait Directives extends ScalaCliCrossSbtModule with ScalaCliPublishModule with HasTests with ScalaCliScalafixModule with LocatedInModules { override def crossScalaVersion: String = crossValue override def moduleDeps: Seq[SonatypeCentralPublishModule] = Seq( options(crossScalaVersion), core(crossScalaVersion), `build-macros`(crossScalaVersion), `specification-level`(crossScalaVersion) ) override def scalacOptions: T[Seq[String]] = Task { super.scalacOptions() ++ asyncScalacOptions(crossScalaVersion) } override def compileMvnDeps: T[Seq[Dep]] = super.compileMvnDeps() ++ Seq( Deps.jsoniterMacros, Deps.svm ) override def mvnDeps: T[Seq[Dep]] = super.mvnDeps() ++ Seq( // Deps.asm, Deps.bloopConfig, Deps.jsoniterCore, Deps.pprint, Deps.usingDirectives ) override def repositoriesTask: Task[Seq[Repository]] = Task.Anon(super.repositoriesTask() ++ deps.customRepositories) object test extends ScalaCliTests { override def mvnDeps: T[Seq[Dep]] = super.mvnDeps() ++ Seq( Deps.pprint ) override def runClasspath: T[Seq[PathRef]] = Task { super.runClasspath() ++ Seq(`local-repo`.localRepoJar()) } override def generatedSources: T[Seq[PathRef]] = super.generatedSources() ++ Seq(constantsFile()) def constantsFile: T[PathRef] = Task(persistent = true) { val dir = Task.dest / "constants" val dest = dir / "Constants2.scala" val code = s"""package scala.build.tests | |/** Build-time constants. Generated by mill. */ |object Constants { | def cs = "${settings.cs().replace("\\", "\\\\")}" |} |""".stripMargin if (!os.isFile(dest) || os.read(dest) != code) os.write.over(dest, code, createFolders = true) PathRef(dir) } // uncomment below to debug tests in attach mode on 5005 port // def forkArgs = Task { // super.forkArgs() ++ Seq("-agentlib:jdwp=transport=dt_socket,server=n,address=localhost:5005,suspend=y") // } } } trait Config extends ScalaCliCrossSbtModule with ScalaCliPublishModule with ScalaCliScalafixModule with LocatedInModules { override def crossScalaVersion: String = crossValue override def moduleDeps: Seq[SonatypeCentralPublishModule] = Seq(`specification-level`(crossScalaVersion)) override def mvnDeps: T[Seq[Dep]] = super.mvnDeps() ++ Seq(Deps.jsoniterCore) override def compileMvnDeps: T[Seq[Dep]] = super.compileMvnDeps() ++ Seq(Deps.jsoniterMacros) override def scalacOptions: T[Seq[String]] = super.scalacOptions() ++ Seq("-deprecation") } trait Options extends ScalaCliCrossSbtModule with ScalaCliPublishModule with HasTests with ScalaCliScalafixModule with LocatedInModules { override def crossScalaVersion: String = crossValue override def moduleDeps: Seq[SonatypeCentralPublishModule] = Seq( core(crossScalaVersion) ) override def compileModuleDeps: Seq[JavaModule] = Seq( `build-macros`(crossScalaVersion) ) override def scalacOptions: T[Seq[String]] = Task { super.scalacOptions() ++ asyncScalacOptions(crossScalaVersion) } override def mvnDeps: T[Seq[Dep]] = super.mvnDeps() ++ Seq( Deps.bloopConfig, Deps.signingCliShared ) override def compileMvnDeps: T[Seq[Dep]] = super.compileMvnDeps() ++ Seq( Deps.jsoniterMacros ) override def repositoriesTask: Task[Seq[Repository]] = Task.Anon(super.repositoriesTask() ++ deps.customRepositories) object test extends ScalaCliTests with ScalaCliScalafixModule { override def scalacOptions = super.scalacOptions() ++ Seq("-deprecation") // uncomment below to debug tests in attach mode on 5005 port // def forkArgs = Task { // super.forkArgs() ++ Seq("-agentlib:jdwp=transport=dt_socket,server=n,address=localhost:5005,suspend=y") // } } } trait Build extends ScalaCliCrossSbtModule with ScalaCliPublishModule with HasTests with ScalaCliScalafixModule with LocatedInModules { override def crossScalaVersion: String = crossValue override def moduleDir: os.Path = super.moduleDir / os.up / "build" override def moduleDeps: Seq[SonatypeCentralPublishModule] = Seq( options(crossScalaVersion), directives(crossScalaVersion), `scala-cli-bsp`, `test-runner`(crossScalaVersion), `tasty-lib`(crossScalaVersion) ) override def scalacOptions: T[Seq[String]] = Task { super.scalacOptions() ++ asyncScalacOptions(crossScalaVersion) } override def compileMvnDeps: T[Seq[Dep]] = super.compileMvnDeps() ++ Seq( Deps.jsoniterMacros, Deps.svm ) override def mvnDeps: T[Seq[Dep]] = super.mvnDeps() ++ Seq( Deps.asm, Deps.collectionCompat, Deps.javaClassName, Deps.jsoniterCore, Deps.scalametaSemanticDbShared, Deps.nativeTestRunner, Deps.osLib, Deps.pprint, Deps.scalaJsEnvNodeJs, Deps.scalaJsTestAdapter, Deps.swoval, Deps.zipInputStream ) override def repositoriesTask: Task[Seq[Repository]] = Task.Anon(super.repositoriesTask() ++ deps.customRepositories) object test extends ScalaCliTests with ScalaCliScalafixModule { override def scalacOptions: T[Seq[String]] = super.scalacOptions() ++ Seq("-deprecation") override def mvnDeps: T[Seq[Dep]] = super.mvnDeps() ++ Seq( Deps.pprint, Deps.slf4jNop ) override def runClasspath: T[Seq[PathRef]] = Task { super.runClasspath() ++ Seq(`local-repo`.localRepoJar()) } override def generatedSources: T[Seq[PathRef]] = super.generatedSources() ++ Seq(constantsFile()) def constantsFile: T[PathRef] = Task(persistent = true) { val dir = Task.dest / "constants" val dest = dir / "Constants2.scala" val code = s"""package scala.build.tests | |/** Build-time constants. Generated by mill. */ |object Constants { | def cs = "${settings.cs().replace("\\", "\\\\")}" | def toolkitOrganization = "${Deps.toolkit.dep.module.organization.value}" | def toolkitName = "${Deps.toolkit.dep.module.name.value}" | def toolkitTestName = "${Deps.toolkitTest.dep.module.name.value}" | def toolkitVersion = "${Deps.toolkitTest.dep.versionConstraint.asString}" | def typelevelToolkitOrganization = "${Deps.typelevelToolkit.dep.module.organization .value}" | def typelevelToolkitVersion = "${Deps.typelevelToolkit.dep.versionConstraint.asString}" | | def defaultScalaVersion = "${Scala.defaultUser}" | def defaultScala212Version = "${Scala.scala212}" | def defaultScala213Version = "${Scala.scala213}" |} |""".stripMargin if (!os.isFile(dest) || os.read(dest) != code) os.write.over(dest, code, createFolders = true) PathRef(dir) } // uncomment below to debug tests in attach mode on 5005 port // def forkArgs = Task { // super.forkArgs() ++ Seq("-agentlib:jdwp=transport=dt_socket,server=n,address=localhost:5005,suspend=y") // } } } trait SpecificationLevel extends ScalaCliCrossSbtModule with ScalaCliPublishModule with LocatedInModules { override def crossScalaVersion: String = crossValue } trait Cli extends CrossSbtModule with ProtoBuildModule with CliLaunchers with FormatNativeImageConf with LocatedInModules { // Copied from Mill: https://github.com/com-lihaoyi/mill/blob/ea367c09bd31a30464ca901cb29863edde5340be/scalalib/src/mill/scalalib/JavaModule.scala#L792 def debug(port: Int, args: Task[Args] = Task.Anon(Args())): Command[Unit] = Task.Command { try mill.api.Result.Success( mill.util.Jvm.callProcess( mainClass = finalMainClass(), classPath = runClasspath().map(_.path), jvmArgs = forkArgs() ++ Seq( s"-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=$port,quiet=y" ), mainArgs = args().value ) ) catch { case _: Exception => mill.api.Result.Failure("subprocess failed") } } def constantsFile: T[PathRef] = Task(persistent = true) { val dir = Task.dest / "constants" val dest = dir / "Constants.scala" val code = s"""package scala.cli.internal | |/** Build-time constants. Generated by mill. */ |object Constants { | def pythonInterfaceOrg = "${Deps.pythonInterface.dep.module.organization.value}" | def pythonInterfaceName = "${Deps.pythonInterface.dep.module.name.value}" | def pythonInterfaceVersion = "${Deps.pythonInterface.dep.versionConstraint.asString}" | def launcherTypeResourcePath = "${launcherTypeResourcePath.toString}" | def defaultFilesResourcePath = "$defaultFilesResourcePath" | def maxAmmoniteScala3Version = "${Scala.maxAmmoniteScala3Version}" | def maxAmmoniteScala3LtsVersion = "${Scala.maxAmmoniteScala3LtsVersion}" |} |""".stripMargin if (!os.isFile(dest) || os.read(dest) != code) os.write.over(dest, code, createFolders = true) PathRef(dir) } def optionsConstantsFile: T[PathRef] = Task(persistent = true) { val dir = Task.dest / "constants" val dest = dir / "Constants.scala" val code = s"""package scala.cli.commands | |/** Build-time constants. Generated by mill. */ |object Constants { | def defaultScalaVersion = "${Scala.defaultUser}" | def defaultJavaVersion = ${Java.defaultJava} | def minimumLauncherJavaVersion = ${Java.minimumJavaLauncherJava} | def minimumBloopJavaVersion = ${Java.minimumBloopJava} | def scalaJsVersion = "${Scala.scalaJs}" | def scalaJsCliVersion = "${Scala.scalaJsCli}" | def scalaNativeVersion = "${Deps.nativeTools.dep.versionConstraint.asString}" | def ammoniteVersion = "${Deps.Versions.ammonite}" | def ammoniteVersionForScala3Lts = "${Deps.Versions.ammoniteForScala3Lts}" | def defaultScalafmtVersion = "${Deps.scalafmtCli.dep.versionConstraint.asString}" | def defaultGraalVMJavaVersion = "${deps.graalVmJavaVersion}" | def defaultGraalVMVersion = "${deps.graalVmCommunityVersion}" | def scalaPyVersion = "${Deps.scalaPy.dep.versionConstraint.asString}" | def signingCliJvmVersion = ${Deps.Versions.signingCliJvmVersion} | def defaultMillVersion = "${BuildInfo.millVersion}" | def mill012Version = "${Deps.Versions.mill012Version}" | def mill1Version = "${Deps.Versions.mill1Version}" | def defaultSbtVersion = "${Deps.Versions.sbtVersion}" | def defaultMavenVersion = "${Deps.Versions.mavenVersion}" | def defaultMavenScalaCompilerPluginVersion = "${Deps.Versions.mavenScalaCompilerPluginVersion}" | def defaultMavenExecPluginVersion = "${Deps.Versions.mavenExecPluginVersion}" |} |""".stripMargin if (!os.isFile(dest) || os.read(dest) != code) os.write.over(dest, code, createFolders = true) PathRef(dir) } override def generatedSources: T[Seq[PathRef]] = super.generatedSources() ++ Seq(constantsFile(), optionsConstantsFile()) def defaultFilesResources: T[PathRef] = Task(persistent = true) { val dir = Task.dest / "resources" def transformWorkflow(content: Array[Byte]): Array[Byte] = new String(content, "UTF-8") .replaceAll(" ./scala-cli", " scala-cli") .getBytes("UTF-8") val resources = Seq[(String, os.SubPath, Array[Byte] => Array[Byte])]( ( "https://raw.githubusercontent.com/scala-cli/default-workflow/main/.github/workflows/ci.yml", os.sub / "workflows" / "default.yml", transformWorkflow ), ( "https://raw.githubusercontent.com/scala-cli/default-workflow/main/.gitignore", os.sub / "gitignore", identity ) ) for ((srcUrl, destRelPath, transform) <- resources) { val dest = dir / defaultFilesResourcePath / destRelPath if (!os.isFile(dest)) { val content = Using.resource(new URL(srcUrl).openStream())(_.readAllBytes()) os.write(dest, transform(content), createFolders = true) } } PathRef(dir) } override def resources: T[Seq[PathRef]] = super.resources() ++ Seq(defaultFilesResources()) override def scalacOptions: T[Seq[String]] = Task { super.scalacOptions() ++ asyncScalacOptions(crossScalaVersion) ++ Seq("-deprecation") } override def javacOptions: T[Seq[String]] = Task { super.javacOptions() ++ Seq("--release", "16") } def moduleDeps: Seq[SonatypeCentralPublishModule] = Seq( `build-module`(crossScalaVersion), config(crossScalaVersion), `specification-level`(crossScalaVersion) ) override def repositoriesTask: Task[Seq[Repository]] = Task.Anon(super.repositoriesTask() ++ customRepositories) override def mvnDeps: T[Seq[Dep]] = super.mvnDeps() ++ Seq( Deps.caseApp, Deps.coursierLauncher, Deps.coursierProxySetup, Deps.coursierPublish.exclude((organization, "config_2.13")).exclude((organization, "config_3")), Deps.jimfs, // scalaJsEnvNodeJs pulls jimfs:1.1, whose class path seems borked (bin compat issue with the guava version it depends on) Deps.jniUtils, Deps.jsoniterCore, Deps.libsodiumjni, Deps.metaconfigTypesafe, Deps.pythonNativeLibs, Deps.scalaPackager.exclude("com.lihaoyi" -> "os-lib_2.13"), Deps.signingCli.exclude((organization, "config_2.13")).exclude((organization, "config_3")), Deps.slf4jNop, // to silence jgit Deps.sttp, Deps.scalafixInterfaces, Deps.scala3Graal, // TODO: drop this if we ever bump internal JDK to 24+ Deps.scala3GraalProcessor // TODO: drop this if we ever bump internal JDK to 24+ ) override def compileMvnDeps: T[Seq[Dep]] = super.compileMvnDeps() ++ Seq( Deps.jsoniterMacros, Deps.svm ) override def mainClass: T[Option[String]] = Some("scala.cli.ScalaCli") private def scala3GraalProcessorClassPath: T[Seq[PathRef]] = Task { defaultResolver().classpath { val bind = bindDependency() Seq(Deps.scala3GraalProcessor).map(bind) } } override def nativeImageClassPath: T[Seq[PathRef]] = Task { val classpath = super.nativeImageClassPath().map(_.path).mkString(File.pathSeparator) val cache = Task.dest / "native-cp" // `scala3-graal-processor`.run() does not give me output and I cannot pass dynamically computed values like classpath // TODO: drop this if we ever bump internal JDK to 24+ val res = mill.util.Jvm.callProcess( mainClass = "scala.cli.graal.CoursierCacheProcessor", classPath = scala3GraalProcessorClassPath().map(_.path).toList, mainArgs = Seq(cache.toNIO.toString, classpath) ) val cp = res.out.trim() cp.split(File.pathSeparator).toSeq.map(p => PathRef(os.Path(p))) } override def localRepoJar: T[PathRef] = `local-repo`.localRepoJar() object test extends ScalaCliTests with ScalaCliScalafixModule { override def moduleDeps: Seq[JavaModule] = super.moduleDeps ++ Seq( `build-module`(crossScalaVersion).test ) override def runClasspath: T[Seq[PathRef]] = Task { super.runClasspath() ++ Seq(localRepoJar()) } override def compileMvnDeps: T[Seq[Dep]] = super.mvnDeps() ++ Seq( Deps.jsoniterMacros ) // Required by the reflection usage in modules/cli/src/test/scala/cli/tests/SetupScalaCLITests.scala override def forkArgs: T[Seq[String]] = Task { super.forkArgs() ++ Seq("--add-opens=java.base/java.util=ALL-UNNAMED") } } } trait CliIntegration extends SbtModule with ScalaCliPublishModule with HasTests with ScalaCliScalafixModule with LocatedInModules { override def scalaVersion: T[String] = sv def sv: String = Scala.scala3Lts def tmpDirBase: T[PathRef] = Task(persistent = true) { PathRef(Task.dest / "working-dir") } override def scalacOptions: T[Seq[String]] = Task { super.scalacOptions() ++ Seq("-deprecation") } override def mvnDeps: T[Seq[Dep]] = super.mvnDeps() ++ Seq( Deps.osLib ) trait IntegrationScalaTests extends super.ScalaCliTests with ScalaCliScalafixModule { override def mvnDeps: T[Seq[Dep]] = super.mvnDeps() ++ Seq( Deps.bsp4j, Deps.coursier .exclude(("com.github.plokhotnyuk.jsoniter-scala", "jsoniter-scala-macros")), Deps.dockerClient, Deps.jsoniterCore, Deps.libsodiumjni, Deps.pprint, Deps.slf4jNop, Deps.usingDirectives ) override def compileMvnDeps: T[Seq[Dep]] = super.compileMvnDeps() ++ Seq( Deps.jsoniterMacros ) override def forkEnv: T[Map[String, String]] = super.forkEnv() ++ Seq( "SCALA_CLI_TMP" -> tmpDirBase().path.toString, "SCALA_CLI_PRINT_STACK_TRACES" -> "1", "SCALA_CLI_CONFIG" -> (tmpDirBase().path / "config" / "config.json").toString ) def constantsFile: T[PathRef] = Task(persistent = true) { val dir = Task.dest / "constants" val dest = dir / "Constants.scala" val mostlyStaticDockerfile = BuildCtx.workspaceRoot / ".github" / "scripts" / "docker" / "ScalaCliSlimDockerFile" assert( os.exists(mostlyStaticDockerfile), s"Error: $mostlyStaticDockerfile not found" ) val code = s"""package scala.cli.integration | |/** Build-time constants. Generated by mill. */ |object Constants { | def cliVersion = "${publishVersion()}" | def allJavaVersions = Seq(${Java.allJavaVersions.sorted.mkString(", ")}) | def bspVersion = "${Deps.bsp4j.dep.versionConstraint.asString}" | def minimumLauncherJavaVersion = ${Java.minimumJavaLauncherJava} | def bloopMinimumJvmVersion = ${Java.minimumBloopJava} | def minimumInternalJvmVersion = ${Java.minimumInternalJava} | def defaultJvmVersion = ${Java.defaultJava} | def scala212 = "${Scala.scala212}" | def scala213 = "${Scala.scala213}" | def scala3LtsPrefix = "${Scala.scala3LtsPrefix}" | def scala3Lts = "${Scala.scala3Lts}" | def scala3NextPrefix = "${Scala.scala3NextPrefix}" | def scala3NextRc = "${Scala.scala3NextRc}" | def scala3NextRcAnnounced = "${Scala.scala3NextRcAnnounced}" | def scala3Next = "${Scala.scala3Next}" | def scala3NextAnnounced = "${Scala.scala3NextAnnounced}" | def defaultScala = "${Scala.defaultUser}" | def scala38Versions = Seq(${Scala.scala38Versions .sorted .map(s => s"\"$s\"") .mkString(", ")}) | def scala38MinJavaVersion = ${Java.minimumScala38Java} | def defaultScalafmtVersion = "${Deps.scalafmtCli.dep.versionConstraint.asString}" | def maxAmmoniteScala212Version = "${Scala.maxAmmoniteScala212Version}" | def maxAmmoniteScala213Version = "${Scala.maxAmmoniteScala213Version}" | def maxAmmoniteScala3Version = "${Scala.maxAmmoniteScala3Version}" | def maxAmmoniteScala3LtsVersion = "${Scala.maxAmmoniteScala3LtsVersion}" | def legacyScala3Versions = Seq(${Scala.legacyScala3Versions.map(p => s"\"$p\"" ).mkString(", ")}) | def scalaJsVersion = "${Scala.scalaJs}" | def scalaJsCliVersion = "${Scala.scalaJsCli}" | def scalaNativeVersion = "${Deps.Versions.scalaNative}" | def scalaNativeVersion04 = "${Deps.Versions.scalaNative04}" | def scalaNativeVersion05 = "${Deps.Versions.scalaNative05}" | def semanticDbJavacPluginVersion = "${Deps.semanticDbJavac.dep.versionConstraint.asString}" | def ammoniteVersion = "${Deps.ammonite.dep.versionConstraint.asString}" | def defaultGraalVMJavaVersion = "${deps.graalVmJavaVersion}" | def defaultGraalVMVersion = "${deps.graalVmCommunityVersion}" | def runnerScala30LegacyVersion = "${Cli.runnerScala30LegacyVersion}" | def runnerScala2LegacyVersion = "${Cli.runnerScala2LegacyVersion}" | def scalaPyVersion = "${Deps.scalaPy.dep.versionConstraint.asString}" | def scalaPyMaxScalaNative = "${Deps.Versions.maxScalaNativeForScalaPy}" | def bloopVersion = "${Deps.bloopRifle.dep.versionConstraint.asString}" | def pprintVersion = "${TestDeps.pprint.dep.versionConstraint.asString}" | def munitVersion = "${TestDeps.munit.dep.versionConstraint.asString}" | def dockerTestImage = "${Docker.testImage}" | def dockerAlpineTestImage = "${Docker.alpineTestImage}" | def authProxyTestImage = "${Docker.authProxyTestImage}" | def mostlyStaticDockerfile = "${mostlyStaticDockerfile.toString.replace( "\\", "\\\\" )}" | def cs = "${settings.cs().replace("\\", "\\\\")}" | def workspaceDirName = "$workspaceDirName" | def libsodiumVersion = "${deps.libsodiumVersion}" | def alpineLibsodiumVersion = "${deps.alpineLibsodiumVersion}" | def condaLibsodiumVersion = "${deps.condaLibsodiumVersion}" | def dockerArchLinuxImage = "${TestDeps.archLinuxImage}" | | def toolkitVersion = "${Deps.toolkitVersion}" | def toolkitVersionForNative04 = "${Deps.toolkitVersionForNative04}" | def toolkitVersionForNative05 = "${Deps.toolkitVersionForNative05}" | def toolkiMaxScalaNative = "${Deps.Versions.maxScalaNativeForToolkit}" | def typelevelToolkitVersion = "${Deps.typelevelToolkitVersion}" | def typelevelToolkitMaxScalaNative = "${Deps.Versions .maxScalaNativeForTypelevelToolkit}" | | def ghOrg = "$ghOrg" | def ghName = "$ghName" | | def jmhVersion = "${Deps.Versions.jmh}" | def jmhOrg = "${Deps.jmhCore.dep.module.organization.value}" | def jmhCoreModule = "${Deps.jmhCore.dep.module.name.value}" | def jmhGeneratorBytecodeModule = "${Deps.jmhGeneratorBytecode.dep.module.name.value}" | def defaultMillVersion = "${BuildInfo.millVersion}" | def mill012Version = "${Deps.Versions.mill012Version}" | def mill1Version = "${Deps.Versions.mill1Version}" |} |""".stripMargin if (!os.isFile(dest) || os.read(dest) != code) os.write.over(dest, code, createFolders = true) PathRef(dir) } override def generatedSources: T[Seq[PathRef]] = super.generatedSources() ++ Seq(constantsFile()) override def testForked(args: String*): Command[(msg: String, results: Seq[TestResult])] = jvm(args*) def forcedLauncher: T[PathRef] = Task(persistent = true) { val ext = if (Properties.isWin) ".exe" else "" val launcher = Task.dest / s"scala-cli$ext" if !os.exists(launcher) then BuildCtx.withFilesystemCheckerDisabled { val dir = Option(System.getenv("SCALA_CLI_IT_FORCED_LAUNCHER_DIRECTORY")).getOrElse { sys.error("SCALA_CLI_IT_FORCED_LAUNCHER_DIRECTORY not set") } System.err.println(s"SCALA_CLI_IT_FORCED_LAUNCHER_DIRECTORY was set to $dir") val content = importedLauncher(dir, BuildCtx.workspaceRoot) System.err.println(s"writing launcher to $launcher") os.write( launcher, content, createFolders = true, perms = if (Properties.isWin) null else "rwxr-xr-x" ) } PathRef(launcher) } def forcedStaticLauncher: T[PathRef] = Task(persistent = true) { val launcher = Task.dest / "scala-cli" if !os.exists(launcher) then BuildCtx.withFilesystemCheckerDisabled { val dir = Option(System.getenv("SCALA_CLI_IT_FORCED_STATIC_LAUNCHER_DIRECTORY")).getOrElse { sys.error("SCALA_CLI_IT_FORCED_STATIC_LAUNCHER_DIRECTORY not set") } System.err.println(s"SCALA_CLI_IT_FORCED_STATIC_LAUNCHER_DIRECTORY was set to $dir") val content = importedLauncher(dir, BuildCtx.workspaceRoot) System.err.println(s"writing launcher to $launcher") os.write(launcher, content, createFolders = true) } PathRef(launcher) } def forcedMostlyStaticLauncher: T[PathRef] = Task(persistent = true) { val launcher = Task.dest / "scala-cli" if !os.exists(launcher) then BuildCtx.withFilesystemCheckerDisabled { val dir = Option(System.getenv("SCALA_CLI_IT_FORCED_MOSTLY_STATIC_LAUNCHER_DIRECTORY")).getOrElse { sys.error("SCALA_CLI_IT_FORCED_MOSTLY_STATIC_LAUNCHER_DIRECTORY not set") } System.err.println( s"SCALA_CLI_IT_FORCED_MOSTLY_STATIC_LAUNCHER_DIRECTORY was set to $dir" ) val content = importedLauncher(dir, BuildCtx.workspaceRoot) System.err.println(s"writing launcher to $launcher") os.write(launcher, content, createFolders = true) } PathRef(launcher) } private object Launchers { def jvm: T[PathRef] = cli(Scala.defaultInternal).standaloneLauncher def jvmBootstrapped: T[PathRef] = cliBootstrapped.jar def native: T[PathRef] = Option(System.getenv("SCALA_CLI_IT_FORCED_LAUNCHER_DIRECTORY")) match { case Some(_) => forcedLauncher case None => cli(Scala.defaultInternal).nativeImage } def nativeStatic: T[PathRef] = Option(System.getenv("SCALA_CLI_IT_FORCED_STATIC_LAUNCHER_DIRECTORY")) match { case Some(_) => forcedStaticLauncher case None => cli(Scala.defaultInternal).nativeImageStatic } def nativeMostlyStatic: T[PathRef] = Option(System.getenv("SCALA_CLI_IT_FORCED_MOSTLY_STATIC_LAUNCHER_DIRECTORY")) match { case Some(_) => forcedMostlyStaticLauncher case None => cli(Scala.defaultInternal).nativeImageMostlyStatic } } private def extraTestArgs(launcher: os.Path, cliKind: String): Seq[String] = Seq( s"-Dtest.scala-cli.path=$launcher", s"-Dtest.scala-cli.kind=$cliKind" ) private def debugTestArgs(args: Seq[String]): Seq[String] = { val debugReg = "^--debug$|^--debug:([0-9]+)$".r val debugPortOpt = args.find(debugReg.matches).flatMap { case debugReg(port) => Option(port).orElse(Some("5005")) case _ => None } debugPortOpt match { case Some(port) => System.err.println( s"--debug option has been passed. Listening for transport dt_socket at address: $port" ) Seq(s"-Dtest.scala-cli.debug.port=$port") case _ => Seq.empty } } private def testArgs(args: Seq[String], launcher: os.Path, cliKind: String): Seq[String] = extraTestArgs(launcher, cliKind) ++ debugTestArgs(args) def jvm(args: String*): Command[(msg: String, results: Seq[TestResult])] = Task.Command { testTask( Task.Anon(args ++ testArgs(args, Launchers.jvm().path, "jvm")), Task.Anon(Seq.empty[String]) )() } def jvmBootstrapped(args: String*): Command[(msg: String, results: Seq[TestResult])] = Task.Command { testTask( Task.Anon(args ++ testArgs(args, Launchers.jvmBootstrapped().path, "jvmBootstrapped")), Task.Anon(Seq.empty[String]) )() } def native(args: String*): Command[(msg: String, results: Seq[TestResult])] = Task.Command { testTask( Task.Anon(args ++ testArgs(args, Launchers.native().path, "native")), Task.Anon(Seq.empty[String]) )() } def nativeStatic(args: String*): Command[(msg: String, results: Seq[TestResult])] = Task.Command { testTask( Task.Anon(args ++ testArgs(args, Launchers.nativeStatic().path, "native-static")), Task.Anon(Seq.empty[String]) )() } def nativeMostlyStatic(args: String*): Command[(msg: String, results: Seq[TestResult])] = Task.Command { testTask( Task.Anon(args ++ testArgs( args, Launchers.nativeMostlyStatic().path, "native-mostly-static" )), Task.Anon(Seq.empty[String]) )() } } } trait CliIntegrationDocker extends SbtModule with ScalaCliPublishModule with HasTests { override def scalaVersion: T[String] = Scala.scala3Lts override def mvnDeps: T[Seq[Dep]] = super.mvnDeps() ++ Seq( Deps.osLib ) } trait Runner extends CrossSbtModule with ScalaCliPublishModule with ScalaCliScalafixModule with LocatedInModules { override def scalacOptions: T[Seq[String]] = Task { super.scalacOptions() ++ Seq("-deprecation") } override def mainClass: T[Option[String]] = Some("scala.cli.runner.Runner") override def sources: T[Seq[PathRef]] = { val scala3DirName = if (crossScalaVersion.contains("-RC")) "scala-3-unstable" else "scala-3-stable" val extraDirs = Seq(PathRef(moduleDir / "src" / "main" / scala3DirName)) super.sources() ++ extraDirs } } trait TestRunner extends CrossSbtModule with ScalaCliPublishModule with ScalaCliScalafixModule with LocatedInModules { override def scalacOptions: T[Seq[String]] = Task { super.scalacOptions() ++ Seq("-deprecation") } override def mvnDeps: T[Seq[Dep]] = super.mvnDeps() ++ Seq( Deps.asm, Deps.collectionCompat, Deps.testInterface ) override def mainClass: T[Option[String]] = Some("scala.build.testrunner.DynamicTestRunner") } trait JavaTestRunner extends JavaModule with ScalaCliPublishModule with LocatedInModules { override def mvnDeps: T[Seq[Dep]] = super.mvnDeps() ++ Seq( Deps.asm, Deps.testInterface ) override def mainClass: T[Option[String]] = Some("scala.build.testrunner.JavaDynamicTestRunner") } trait TastyLib extends ScalaCliCrossSbtModule with ScalaCliPublishModule with ScalaCliScalafixModule with LocatedInModules { override def crossScalaVersion: String = crossValue def constantsFile: T[PathRef] = Task(persistent = true) { val dir = Task.dest / "constants" val dest = dir / "Constants.scala" val code = s"""package scala.build.tastylib.internal | |/** Build-time constants. Generated by mill. */ |object Constants { | def defaultScalaVersion = "${Scala.defaultUser}" |} |""".stripMargin if (!os.isFile(dest) || os.read(dest) != code) os.write.over(dest, code, createFolders = true) PathRef(dir) } override def generatedSources: T[Seq[PathRef]] = super.generatedSources() ++ Seq(constantsFile()) } object `local-repo` extends LocalRepo { /* * If you are developing locally on any of the stub modules (stubs, runner, test-runner), * set this to true, so that Mill's watch mode takes into account changes in those modules * when embedding their JARs in the Scala CLI launcher. */ def developingOnStubModules = false override def stubsModules: Seq[PublishLocalNoFluff] = Seq(runner(Scala.runnerScala3), `test-runner`(Scala.runnerScala3), `java-test-runner`) override def version: T[String] = runner(Scala.runnerScala3).publishVersion() } // Helper CI commands def publishSonatype(tasks: Tasks[PublishModule.PublishData]) = Task.Command { val taskNames = tasks.value.map(_.toString()) System.err.println( s"""Tasks producing artifacts to be included in the bundle: | ${taskNames.mkString("\n ")}""".stripMargin ) val pv = finalPublishVersion() System.err.println(s"Publish version: $pv") val bundleName = s"$organization-$ghName-$pv" System.err.println(s"Publishing bundle: $bundleName") publish.publishSonatype( data = Task.sequence(tasks.value)(), log = Task.ctx().log, workspace = BuildCtx.workspaceRoot, env = Task.env, bundleName = bundleName ) } def copyTo(task: Tasks[PathRef], dest: String): Command[Unit] = Task.Command { val destPath = os.Path(dest, BuildCtx.workspaceRoot) if (task.value.length > 1) sys.error("Expected a single task") val ref = task.value.head() os.makeDir.all(destPath / os.up) os.copy.over(ref.path, destPath) } def writePackageVersionTo(dest: String): Command[Unit] = Task.Command { val destPath = os.Path(dest, BuildCtx.workspaceRoot) val rawVersion = cli(Scala.defaultInternal).publishVersion() val version = if (rawVersion.contains("+")) rawVersion.stripSuffix("-SNAPSHOT") else rawVersion os.write.over(destPath, version) } def writeShortPackageVersionTo(dest: String): Command[Unit] = Task.Command { val destPath = os.Path(dest, BuildCtx.workspaceRoot) val rawVersion = cli(Scala.defaultInternal).publishVersion() val version = rawVersion.takeWhile(c => c != '-' && c != '+') os.write.over(destPath, version) } def importedLauncher(directory: String = "artifacts", workspace: os.Path): Array[Byte] = { val ext = if (Properties.isWin) ".zip" else ".gz" val from = os.Path(directory, workspace) / s"scala-cli-${Upload.platformSuffix}$ext" System.err.println(s"Importing launcher from $from") if (!os.exists(from)) sys.error(s"$from not found") if (Properties.isWin) { import java.util.zip.ZipFile Using.resource(new ZipFile(from.toIO)) { zf => val ent = zf.getEntry("scala-cli.exe") Using.resource(zf.getInputStream(ent)) { is => is.readAllBytes() } } } else { import java.io.ByteArrayInputStream import java.util.zip.GZIPInputStream val compressed = os.read.bytes(from) val bais = new ByteArrayInputStream(compressed) Using.resource(new GZIPInputStream(bais)) { gzis => gzis.readAllBytes() } } } def copyLauncher(directory: String = "artifacts"): Command[os.Path] = Task.Command { val nativeLauncher = cli(Scala.defaultInternal).nativeImage().path Upload.copyLauncher0( nativeLauncher = nativeLauncher, directory = directory, name = "scala-cli", compress = true, workspace = BuildCtx.workspaceRoot ) } def copyJvmLauncher(directory: String = "artifacts"): Command[Unit] = Task.Command { val launcher = cli(Scala.defaultInternal).standaloneLauncher().path os.copy( launcher, os.Path(directory, BuildCtx.workspaceRoot) / s"scala-cli$platformExecutableJarExtension", createFolders = true, replaceExisting = true ) } def copyJvmBootstrappedLauncher(directory: String = "artifacts"): Command[Unit] = Task.Command { val launcher = cliBootstrapped.jar().path os.copy( launcher, os.Path(directory, BuildCtx.workspaceRoot) / s"scala-cli.jar", createFolders = true, replaceExisting = true ) } def uploadLaunchers(directory: String = "artifacts"): Command[Unit] = Task.Command { val version = cli(Scala.defaultInternal).publishVersion() val path = os.Path(directory, BuildCtx.workspaceRoot) val launchers = os.list(path).filter(os.isFile(_)).map { path => path -> path.last } val (tag, overwriteAssets) = if (version.endsWith("-SNAPSHOT")) ("nightly", true) else ("v" + version, false) System.err.println(s"Uploading to tag $tag (overwrite assets: $overwriteAssets)") Upload.upload(ghOrg, ghName, ghToken(), tag, dryRun = false, overwrite = overwriteAssets)( launchers* ) } def unitTests(): Command[(msg: String, results: Seq[TestResult])] = Task.Command { `build-module`(Scala.defaultInternal).test.testForked()() `build-macros`(Scala.defaultInternal).test.testForked()() cli(Scala.defaultInternal).test.testForked()() directives(Scala.defaultInternal).test.testForked()() options(Scala.defaultInternal).test.testForked()() } def scala(args: Task[Args] = Task.Anon(Args())) = Task.Command { cli(Scala.defaultInternal).run(args)() } def debug(port: Int, args: Task[Args] = Task.Anon(Args())) = Task.Command { cli(Scala.defaultInternal).debug(port, args)() } def defaultNativeImage(): Command[PathRef] = Task.Command { cli(Scala.defaultInternal).nativeImage() } def nativeIntegrationTests(): Command[(msg: String, results: Seq[TestResult])] = integration.test.native() def copyDefaultLauncher(directory: String = "artifacts"): Command[os.Path] = Task.Command { copyLauncher(directory)() } def copyMostlyStaticLauncher(directory: String = "artifacts"): Command[os.Path] = Task.Command { val nativeLauncher = cli(Scala.defaultInternal).nativeImageMostlyStatic().path Upload.copyLauncher0( nativeLauncher = nativeLauncher, directory = directory, name = "scala-cli", compress = true, workspace = BuildCtx.workspaceRoot, suffix = "-mostly-static" ) } def copyStaticLauncher(directory: String = "artifacts"): Command[os.Path] = Task.Command { val nativeLauncher = cli(Scala.defaultInternal).nativeImageStatic().path Upload.copyLauncher0( nativeLauncher = nativeLauncher, directory = directory, name = "scala-cli", compress = true, workspace = BuildCtx.workspaceRoot, suffix = "-static" ) } private def ghToken(): String = Option(System.getenv("UPLOAD_GH_TOKEN")).getOrElse { sys.error("UPLOAD_GH_TOKEN not set") } private def gitClone(repo: String, branch: String, workDir: os.Path): os.CommandResult = os.proc("git", "clone", repo, "-q", "-b", branch).call(cwd = workDir) private def setupGithubRepo(repoDir: os.Path): os.CommandResult = { val gitUserName = "gh-actions" val gitEmail = "actions@github.com" os.proc("git", "config", "user.name", gitUserName).call(cwd = repoDir) os.proc("git", "config", "user.email", gitEmail).call(cwd = repoDir) } private def commitChanges( name: String, branch: String, repoDir: os.Path, force: Boolean = false ): Unit = { if (os.proc("git", "status").call(cwd = repoDir).out.trim().contains("nothing to commit")) println("Nothing Changes") else { os.proc("git", "add", "-A").call(cwd = repoDir) os.proc("git", "commit", "-am", name).call(cwd = repoDir) println(s"Trying to push on $branch branch") val pushExtraOptions = if (force) Seq("--force") else Seq.empty os.proc("git", "push", "origin", branch, pushExtraOptions).call(cwd = repoDir) println(s"Push successfully on $branch branch") } } // TODO Move most CI-specific tasks there object ci extends Module { def publishVersion(): Command[Unit] = Task.Command { println(cli(Scala.defaultInternal).publishVersion()) } def updateScalaCliSetup(): Command[Unit] = Task.Command { val version = cli(Scala.defaultInternal).publishVersion() val targetDir = BuildCtx.workspaceRoot / "target-scala-cli-setup" val mainDir = targetDir / "scala-cli-setup" val setupScriptPath = mainDir / "src" / "main.ts" // clean target directory if (os.exists(targetDir)) os.remove.all(targetDir) os.makeDir.all(targetDir) val branch = "main" val targetBranch = s"update-scala-cli-setup" val repo = "git@github.com:VirtusLab/scala-cli-setup.git" // Cloning gitClone(repo, branch, targetDir) setupGithubRepo(mainDir) val setupScript = os.read(setupScriptPath) val scalaCliVersionRegex = "const scalaCLIVersion = '.*'".r val updatedSetupScriptPath = scalaCliVersionRegex.replaceFirstIn(setupScript, s"const scalaCLIVersion = '$version'") os.write.over(setupScriptPath, updatedSetupScriptPath) os.proc("git", "switch", "-c", targetBranch).call(cwd = mainDir) commitChanges(s"Update scala-cli version to $version", targetBranch, mainDir, force = true) } def updateStandaloneLauncher(): Command[os.CommandResult] = Task.Command { val version = cli(Scala.defaultInternal).publishVersion() val targetDir = BuildCtx.workspaceRoot / "target" val scalaCliDir = targetDir / "scala-cli" val standaloneLauncherPath = scalaCliDir / "scala-cli.sh" val standaloneWindowsLauncherPath = scalaCliDir / "scala-cli.bat" // clean scala-cli directory if (os.exists(scalaCliDir)) os.remove.all(scalaCliDir) if (!os.exists(targetDir)) os.makeDir.all(targetDir) val branch = "main" val targetBranch = s"update-standalone-launcher-$version" val repo = s"https://oauth2:${ghToken()}@github.com/$ghOrg/$ghName.git" // Cloning gitClone(repo, branch, targetDir) setupGithubRepo(scalaCliDir) val launcherScript = os.read(standaloneLauncherPath) val scalaCliVersionRegex = "SCALA_CLI_VERSION=\".*\"".r val updatedLauncherScript = scalaCliVersionRegex.replaceFirstIn(launcherScript, s"""SCALA_CLI_VERSION="$version"""") os.write.over(standaloneLauncherPath, updatedLauncherScript) val launcherWindowsScript = os.read(standaloneWindowsLauncherPath) val scalaCliWindowsVersionRegex = "SCALA_CLI_VERSION=.*\"".r val updatedWindowsLauncherScript = scalaCliWindowsVersionRegex.replaceFirstIn( launcherWindowsScript, s"""SCALA_CLI_VERSION=$version"""" ) os.write.over(standaloneWindowsLauncherPath, updatedWindowsLauncherScript) os.proc("git", "switch", "-c", targetBranch).call(cwd = scalaCliDir) commitChanges(s"Update scala-cli.sh launcher for $version", targetBranch, scalaCliDir) os.proc("gh", "auth", "login", "--with-token").call(cwd = scalaCliDir, stdin = ghToken()) os.proc("gh", "pr", "create", "--fill", "--base", "main", "--head", targetBranch) .call(cwd = scalaCliDir) } def brewLaunchersSha( x86LauncherPath: os.Path, arm64LauncherPath: os.Path, targetDir: os.Path ): (String, String) = { val x86BinarySha256 = os.proc("openssl", "dgst", "-sha256", "-binary") .call( cwd = targetDir, stdin = os.read.stream(x86LauncherPath) ).out.bytes val arm64BinarySha256 = os.proc("openssl", "dgst", "-sha256", "-binary") .call( cwd = targetDir, stdin = os.read.stream(arm64LauncherPath) ).out.bytes val x86Sha256 = os.proc("xxd", "-p", "-c", "256") .call( cwd = targetDir, stdin = x86BinarySha256 ).out.trim() val arm64Sha256 = os.proc("xxd", "-p", "-c", "256") .call( cwd = targetDir, stdin = arm64BinarySha256 ).out.trim() (x86Sha256, arm64Sha256) } def updateScalaCliBrewFormula(): Command[Unit] = Task.Command { val version = cli(Scala.defaultInternal).publishVersion() val targetDir = BuildCtx.workspaceRoot / "target" val homebrewFormulaDir = targetDir / "homebrew-scala-cli" // clean target directory if (os.exists(targetDir)) os.remove.all(targetDir) os.makeDir.all(targetDir) val branch = "main" val repo = s"git@github.com:Virtuslab/homebrew-scala-cli.git" // Cloning gitClone(repo, branch, targetDir) setupGithubRepo(homebrewFormulaDir) val x86LauncherURL = s"https://github.com/Virtuslab/scala-cli/releases/download/v$version/scala-cli-x86_64-apple-darwin.gz" val arm64LauncherURL = s"https://github.com/Virtuslab/scala-cli/releases/download/v$version/scala-cli-aarch64-apple-darwin.gz" val x86LauncherPath = os.Path("artifacts", BuildCtx.workspaceRoot) / "scala-cli-x86_64-apple-darwin.gz" val arm64LauncherPath = os.Path("artifacts", BuildCtx.workspaceRoot) / "scala-cli-aarch64-apple-darwin.gz" val (x86Sha256, arm64Sha256) = brewLaunchersSha(x86LauncherPath, arm64LauncherPath, targetDir) val templateFormulaPath = BuildCtx.workspaceRoot / ".github" / "scripts" / "scala-cli.rb.template" val template = os.read(templateFormulaPath) val updatedFormula = template .replace("@X86_LAUNCHER_URL@", x86LauncherURL) .replace("@ARM64_LAUNCHER_URL@", arm64LauncherURL) .replace("@X86_LAUNCHER_SHA256@", x86Sha256) .replace("@ARM64_LAUNCHER_SHA256@", arm64Sha256) .replace("@LAUNCHER_VERSION@", version) val formulaPath = homebrewFormulaDir / "scala-cli.rb" os.write.over(formulaPath, updatedFormula) commitChanges(s"Update for $version", branch, homebrewFormulaDir) } def updateScalaExperimentalBrewFormula(): Command[Unit] = Task.Command { val version = cli(Scala.defaultInternal).publishVersion() val targetDir = BuildCtx.workspaceRoot / "target" val homebrewFormulaDir = targetDir / "homebrew-scala-experimental" // clean homebrew-scala-experimental directory if (os.exists(homebrewFormulaDir)) os.remove.all(homebrewFormulaDir) os.makeDir.all(targetDir) val branch = "main" val repo = s"git@github.com:VirtusLab/homebrew-scala-experimental.git" // Cloning gitClone(repo, branch, targetDir) setupGithubRepo(homebrewFormulaDir) val x86LauncherURL = s"https://github.com/Virtuslab/scala-cli/releases/download/v$version/scala-cli-x86_64-apple-darwin.gz" val arm64LauncherURL = s"https://github.com/Virtuslab/scala-cli/releases/download/v$version/scala-cli-aarch64-apple-darwin.gz" val x86LauncherPath = os.Path("artifacts", BuildCtx.workspaceRoot) / "scala-cli-x86_64-apple-darwin.gz" val arm64LauncherPath = os.Path("artifacts", BuildCtx.workspaceRoot) / "scala-cli-aarch64-apple-darwin.gz" val (x86Sha256, arm64Sha256) = brewLaunchersSha(x86LauncherPath, arm64LauncherPath, targetDir) val templateFormulaPath = BuildCtx.workspaceRoot / ".github" / "scripts" / "scala.rb.template" val template = os.read(templateFormulaPath) val updatedFormula = template .replace("@X86_LAUNCHER_URL@", x86LauncherURL) .replace("@ARM64_LAUNCHER_URL@", arm64LauncherURL) .replace("@X86_LAUNCHER_SHA256@", x86Sha256) .replace("@ARM64_LAUNCHER_SHA256@", arm64Sha256) .replace("@LAUNCHER_VERSION@", version) val formulaPath = homebrewFormulaDir / "scala.rb" os.write.over(formulaPath, updatedFormula) commitChanges(s"Update for $version", branch, homebrewFormulaDir) } def updateInstallationScript(): Command[Unit] = Task.Command { val version = cli(Scala.defaultInternal).publishVersion() val targetDir = BuildCtx.workspaceRoot / "target" val packagesDir = targetDir / "scala-cli-packages" val installationScriptPath = packagesDir / "scala-setup.sh" // clean target directory if (os.exists(targetDir)) os.remove.all(targetDir) os.makeDir.all(targetDir) val branch = "main" val repo = s"git@github.com:Virtuslab/scala-cli-packages.git" // Cloning gitClone(repo, branch, targetDir) setupGithubRepo(packagesDir) val installationScript = os.read(installationScriptPath) val scalaCliVersionRegex = "SCALA_CLI_VERSION=\".*\"".r val updatedInstallationScript = scalaCliVersionRegex.replaceFirstIn(installationScript, s"""SCALA_CLI_VERSION="$version"""") os.write.over(installationScriptPath, updatedInstallationScript) commitChanges(s"Update installation script for $version", branch, packagesDir) } def updateDebianPackages(): Command[Unit] = Task.Command { val version = cli(Scala.defaultInternal).publishVersion() val targetDir = BuildCtx.workspaceRoot / "target" val packagesDir = targetDir / "scala-cli-packages" val debianDir = packagesDir / "debian" // clean target directory if (os.exists(targetDir)) os.remove.all(targetDir) os.makeDir.all(targetDir) val branch = "main" val repo = s"git@github.com:Virtuslab/scala-cli-packages.git" // Cloning gitClone(repo, branch, targetDir) setupGithubRepo(packagesDir) // copy deb package to repository os.copy( os.Path("artifacts", BuildCtx.workspaceRoot) / "scala-cli-x86_64-pc-linux.deb", debianDir / s"scala-cli_$version.deb" ) val packagesPath = debianDir / "Packages" os.proc("dpkg-scanpackages", "--multiversion", ".").call(cwd = debianDir, stdout = packagesPath) os.proc("gzip", "-k", "-f", "Packages").call(cwd = debianDir) val releasePath = debianDir / "Release" os.proc("apt-ftparchive", "release", ".").call(cwd = debianDir, stdout = releasePath) val pgpPassphrase = Option(System.getenv("PGP_PASSPHRASE")).getOrElse(sys.error("PGP_PASSPHRASE not set")) val keyName = Option(System.getenv("GPG_EMAIL")).getOrElse(sys.error("GPG_EMAIL not set")) val releaseGpgPath = debianDir / "Release.gpg" val inReleasePath = debianDir / "InRelease" os.proc( "gpg", "--batch", "--yes", "--passphrase-fd", "0", "--default-key", keyName, "-abs", "-o", "-", "Release" ) .call(cwd = debianDir, stdin = pgpPassphrase, stdout = releaseGpgPath) os.proc( "gpg", "--batch", "--yes", "--passphrase-fd", "0", "--default-key", keyName, "--clearsign", "-o", "-", "Release" ) .call(cwd = debianDir, stdin = pgpPassphrase, stdout = inReleasePath) // Export the public key as a binary (non-armored) keyring at the repo root so that // users can reference it via `signed-by` in their APT sources. // Binary format is required per https://wiki.debian.org/DebianRepository/UseThirdParty val keyringPath = packagesDir / "scala-cli-archive-keyring.gpg" os.proc("gpg", "--batch", "--yes", "--export", keyName) .call(stdout = keyringPath) // Update the .list file to include the signed-by option pointing at the keyring. // This scopes the key to this repository only, preventing the globally-trusted key // security issue described in the Debian wiki. os.write.over( debianDir / "scala_cli_packages.list", "deb [signed-by=/etc/apt/keyrings/scala-cli-archive-keyring.gpg] https://virtuslab.github.io/scala-cli-packages/debian ./\n" ) // Also provide a DEB822 .sources file for users on modern Debian (apt modernize-sources). // No Components field: this is a flat repository (Suites: ./). // No Architectures field: avoids breaking when aarch64 packages are added later. os.write.over( debianDir / "scala_cli_packages.sources", """Types: deb |URIs: https://virtuslab.github.io/scala-cli-packages/debian |Suites: ./ |Signed-By: /etc/apt/keyrings/scala-cli-archive-keyring.gpg |""".stripMargin ) commitChanges(s"Update Debian packages for $version", branch, packagesDir) } def updateChocolateyPackage(): Command[os.CommandResult] = Task.Command { val version = cli(Scala.defaultInternal).publishVersion() val packagesDir = BuildCtx.workspaceRoot / "target" / "scala-cli-packages" val chocoDir = BuildCtx.workspaceRoot / ".github" / "scripts" / "choco" val msiPackagePath = packagesDir / s"scala-cli_$version.msi" os.copy( BuildCtx.workspaceRoot / "artifacts" / "scala-cli-x86_64-pc-win32.msi", msiPackagePath, createFolders = true ) // prepare ps1 file val ps1Path = chocoDir / "tools" / "chocolateyinstall.ps1" val launcherURL = s"https://github.com/VirtusLab/scala-cli/releases/download/v$version/scala-cli-x86_64-pc-win32.msi" val bytes = os.read.stream(msiPackagePath) val sha256 = os.proc("sha256sum").call(cwd = packagesDir, stdin = bytes).out.trim().take(64) val ps1UpdatedContent = os.read(ps1Path) .replace("@LAUNCHER_URL@", launcherURL) .replace("@LAUNCHER_SHA256@", sha256.toUpperCase) os.write.over(ps1Path, ps1UpdatedContent) // prepare nuspec file val nuspecPath = chocoDir / "scala-cli.nuspec" val nuspecUpdatedContent = os.read(nuspecPath).replace("@LAUNCHER_VERSION@", version) os.write.over(nuspecPath, nuspecUpdatedContent) os.proc("choco", "pack", nuspecPath, "--out", chocoDir).call(cwd = chocoDir) val chocoKey = Option(System.getenv("CHOCO_SECRET")).getOrElse(sys.error("CHOCO_SECRET not set")) os.proc( "choco", "push", chocoDir / s"scala-cli.$version.nupkg", "-s", "https://push.chocolatey.org/", "-k", chocoKey ).call(cwd = chocoDir) } def updateCentOsPackages(): Command[Unit] = Task.Command { val version = cli(Scala.defaultInternal).publishVersion() val targetDir = BuildCtx.workspaceRoot / "target" val packagesDir = targetDir / "scala-cli-packages" val centOsDir = packagesDir / "CentOS" // clean target directory if (os.exists(targetDir)) os.remove.all(targetDir) os.makeDir.all(targetDir) val branch = "main" val repo = s"git@github.com:Virtuslab/scala-cli-packages.git" // Cloning gitClone(repo, branch, targetDir) setupGithubRepo(packagesDir) // copy rpm package to repository os.copy( os.Path("artifacts", BuildCtx.workspaceRoot) / "scala-cli-x86_64-pc-linux.rpm", centOsDir / "Packages" / s"scala-cli_$version.rpm" ) val cmd = Seq[os.Shellable]( "docker", "run", "-v", s"$packagesDir:/packages", "-w", "/packages", "--env", "PGP_SECRET", "--env", "PGP_PASSPHRASE", "--env", "GPG_EMAIL", "--env", "KEYGRIP", "--privileged", "fedora:40", "sh", "updateCentOsPackages.sh" ) os.proc(cmd).call(cwd = packagesDir) commitChanges(s"Update CentOS packages for $version", branch, packagesDir) } private def vsBasePaths: Seq[os.Path] = Seq( os.Path("C:\\Program Files\\Microsoft Visual Studio"), os.Path("C:\\Program Files (x86)\\Microsoft Visual Studio") ) def copyVcRedist( directory: String = "artifacts", distName: String = "vc_redist.x64.exe" ): Command[Unit] = Task.Command { def vcVersions = Seq("2022", "2019", "2017") def vcEditions = Seq("Enterprise", "Community", "BuildTools") def candidateBaseDirs = for { vsBasePath <- vsBasePaths year <- vcVersions edition <- vcEditions } yield vsBasePath / year / edition / "VC" / "Redist" / "MSVC" val baseDirs = candidateBaseDirs.filter(os.isDir(_)) if (baseDirs.isEmpty) sys.error( s"No Visual Studio installation found, tried:" + System.lineSeparator() + candidateBaseDirs .map(" " + _) .mkString(System.lineSeparator()) ) val orig = baseDirs .iterator .flatMap(os.list(_).iterator) .filter(os.isDir(_)) .map(_ / distName) .filter(os.isFile(_)) .take(1) .toList .headOption .getOrElse { sys.error( s"Error: $distName not found under any of:" + System.lineSeparator() + baseDirs .map(" " + _) .mkString(System.lineSeparator()) ) } val destDir = os.Path(directory, BuildCtx.workspaceRoot) os.copy(orig, destDir / distName, createFolders = true, replaceExisting = true) } def writeWixConfigExtra(dest: String = "wix-visual-cpp-redist.xml"): Command[Unit] = Task.Command { val msmPath = { val vcVersions = Seq("2022", "2019", "2017") val vcEditions = Seq("Enterprise", "Community", "BuildTools") val vsDirs = Seq( os.Path("""C:\Program Files\Microsoft Visual Studio"""), os.Path("""C:\Program Files (x86)\Microsoft Visual Studio""") ) val fileNamePrefix = "Microsoft_VC".toLowerCase(Locale.ROOT) val fileNameSuffix = "_CRT_x64.msm".toLowerCase(Locale.ROOT) def candidatesIt: Iterator[os.Path] = for { vsDir <- vsDirs.iterator version <- vcVersions.iterator edition <- vcEditions.iterator dir = vsDir / version / edition if os.isDir(dir) path <- os.walk.stream(dir) .filter { p => p.last.toLowerCase(Locale.ROOT).startsWith(fileNamePrefix) && p.last.toLowerCase(Locale.ROOT).endsWith(fileNameSuffix) } .filter(os.isFile(_)) .toVector .iterator } yield path candidatesIt.take(1).toList.headOption.getOrElse { sys.error(s"$fileNamePrefix*$fileNameSuffix not found") } } val content = s""" | | | | | |""".stripMargin val dest0 = os.Path(dest, BuildCtx.workspaceRoot) os.write.over(dest0, content.getBytes(Charset.defaultCharset()), createFolders = true) } def setShouldPublish(): Command[Unit] = publish.setShouldPublish() def shouldPublish(): Command[Unit] = Task.Command { println(publish.shouldPublish()) } def copyJvm(jvm: String = deps.graalVmJvmId, dest: String = "jvm"): Command[os.Path] = Task.Command { import sys.process._ val command = Seq( settings.cs(), "java-home", "--jvm", jvm, "--update", "--ttl", "0" ) val baseJavaHome = os.Path(command.!!.trim, BuildCtx.workspaceRoot) System.err.println(s"Initial Java home $baseJavaHome") val destJavaHome = os.Path(dest, BuildCtx.workspaceRoot) os.copy(baseJavaHome, destJavaHome, createFolders = true) System.err.println(s"New Java home $destJavaHome") destJavaHome } def checkScalaVersions(): Command[Unit] = Task.Command { website.checkMainScalaVersions( BuildCtx.workspaceRoot / "website" / "docs" / "reference" / "scala-versions.md" ) website.checkScalaJsVersions( BuildCtx.workspaceRoot / "website" / "docs" / "guides" / "advanced" / "scala-js.md" ) } } ================================================ FILE: gcbenchmark/.gitignore ================================================ tmp-* ================================================ FILE: gcbenchmark/README.md ================================================ Simple tool to analyze memory usage of bloop running with certain JVM options ================================================ FILE: gcbenchmark/gcbenchmark.scala ================================================ //> using dep com.lihaoyi::os-lib:0.9.1 //> using dep com.lihaoyi::pprint:0.9.6 //> using scala 2.13 // Usage: scala-cli gcbenchmark.scala -- import scala.concurrent.duration._ import scala.collection.JavaConverters._ case class Result( env: Map[String, String], maxTime: Duration, avgTime: Duration, maxMemoryFootprintMb: Int, idleMemoryFootprintMb: Int ) object Main { val workspace = os.temp.dir(os.pwd, "tmp-", deleteOnExit = true) // where the temporary files are stored val projectSize = 200 // Number of files in a generated project used in benchmark val numberOfBuilds = 10 // How many times run build for each setup val idleWait = 90 // In seconds. Wait after builds are done, to measure how much memory JVM returns to OS val setups = Seq( Map("BLOOP_JAVA_OPTS" -> "-XX:+UseParallelGC"), Map( "BLOOP_JAVA_OPTS" -> "-XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC -XX:ShenandoahUncommitDelay=30000" ), Map.empty[String, String] ) def bloopMemory(bloopPid: Int) = os .proc("ps", "-o", "rss", bloopPid) .call() .out .text() .linesIterator .toList(1) .toInt / 1024 def bloopPid: Option[Int] = { val processes = os.proc("jps", "-l").call().out.text() "(\\d+) bloop[.]BloopServer".r .findFirstMatchIn(processes) .map(_.group(1).toInt) } def scalaFile(objectName: String, rand: Int) = s""" |object $objectName { |def donothing(i:Int) = {} | donothing($rand) |${" donothing(1+1)\n" * 1000} |} |""".stripMargin def build(scalaCli: String, rand: Int, env: Map[String, String]): Duration = { val classes = (1 to projectSize).map(i => s"Bench$i") for { c <- classes } os.write.over( workspace / s"$c.scala", scalaFile(s"$c", 1000 + rand) ) val start = System.nanoTime() os.proc( "java", "-jar", scalaCli, "compile", workspace ).call(cwd = workspace, env = env, stdout = os.Inherit) val stop = System.nanoTime() val elapsed = 1.0 * (stop - start) / 1000000000 elapsed.seconds } def main(args: Array[String]): Unit = { val scalaCli = pprint.log(args(0)) os.proc("java", "-version").call(stdout = os.Inherit) val results = for { env <- setups } yield { pprint.log(env) bloopPid.foreach(p => os.proc("kill", p).call(stderr = os.Inherit)) Thread.sleep(3000) println("=" * 80) build( scalaCli, 0, env ++ System.getenv.asScala ) val buildResults = (1 to numberOfBuilds).map { i => val elapsed = build(scalaCli, i, env ++ System.getenv.asScala) val memory = bloopMemory(bloopPid.get) pprint.log(f"$memory MB, $elapsed") (memory, elapsed) } val idleResults = for { i <- 1 to idleWait } yield { Thread.sleep(1000) val memory = bloopMemory(bloopPid.get) pprint.log(s"$memory MB") memory } val res = Result( env = env, maxTime = buildResults.map(_._2).max.toSeconds.seconds, avgTime = (buildResults .map(_._2) .fold(0.seconds)(_ + _) / buildResults.size).toSeconds.seconds, maxMemoryFootprintMb = buildResults.map(_._1).max, idleMemoryFootprintMb = idleResults.min ) res } println("=" * 80) pprint.log(results) } } ================================================ FILE: gifs/Dockerfile ================================================ FROM ubuntu:24.04 RUN curl -fsSL https://deb.nodesource.com/setup_12.x | bash - RUN apt-get update RUN apt-get install ca-certificates-java openjdk-17-jdk openjdk-17-jre -y RUN DEBIAN_FRONTEND=noninteractive apt-get install -y pv curl clang rubygems nodejs python3-pip &&\ rm -rf /var/lib/apt/lists/* &&\ gem install rouge &&\ pip3 install --break-system-packages asciinema==2.1.0 RUN mkdir /data WORKDIR /data COPY scala-cli-jvm /usr/bin/scala-cli # Preload scala-cli RUN cd /data RUN echo 'def a = 123' > a.scala RUN scala-cli compile a.scala || echo "Problems with bloop" COPY *.sh /data/ COPY scenarios /data/scenarios ENTRYPOINT ./run_scenario.sh "$1" ================================================ FILE: gifs/README.md ================================================ # Tooling to generate nice gifs used in documentation Recordings are possible using https://github.com/paxtonhare/demo-magic licensed under MIT Licence ## How does it work? Our animated svgs are compose of scenarios built using [demo-magic](https://github.com/paxtonhare/demo-magic) and then recorded using [asciinema](https://asciinema.org/) to .json files to be finally rendered to animated svg files using [svg_rendrer_cli](https://github.com/marionebl/svg-term-cli) ## How to (re)create new gif 1. Copy example.sh into scenarios directory (e.g. `better-error.sh`) 2. Edit its content based on included tips. 3. Run `./mill -i docs-tests.test "sclicheck.GifTests.better-error"` to to (re)render svgs based on `better-error.sh` scenario Gifs will be saved in `website/static/img/gifs` and `website/static/img/dark/gifs` directories based on name of the scenario (so `foo.sh` becomes `foo.svg`) ================================================ FILE: gifs/create_missing.sc ================================================ #!/usr/bin/env scala-cli //> using lib com.lihaoyi::os-lib:0.7.8 /** Small and handy script to generate stubs for .svg files with nice TODO */ val content = os.read(os.pwd / "website" / "src" / "components" / "features.js") val Image = """.*image="([^ ]+)" +(title="(.+)")?.*""".r def needStub(name: String) = !os.exists(os.pwd / "website" / "static" / "img" / name) && name.endsWith(".svg") // Look for missing .svg files in out feature list val stubs = content.linesIterator.collect { case Image(image, _, title) if needStub(image) => image -> s"showing $title" case Image(image) if needStub(image) => image -> "" }.toList // or provide data manually // val stubs = Seq( // ("education", "Scala CLI within scope of learning Scala"), // ("scripting", "scripting with Scala CLI"), // ("prototyping", "Scala CLI used to prototype, experiment and debug"), // ("projects", "Scala CLI to manage single-module projects"), // ("demo", "general demo of Scala CLI"), // ) if stubs.nonEmpty then val scriptBase = os.read(os.pwd / "gifs" / "example.sh") stubs.foreach { case (imageName, desc) => val scriptName = imageName.stripSuffix(".svg") + ".sh" val dest = os.pwd / "gifs" / "scenarios" / scriptName val fullDescr = s"TODO: turn gifs/scenarios/$scriptName into proper scenario $desc" os.write.over(dest, scriptBase.replace("", fullDescr)) os.perms.set(dest, "rwxr-xr-x") println(s"Wrote: $dest") } val names = stubs.map(_._1.stripSuffix(".svg")).mkString(" ") println(s"To generate svg files run: 'gifs/generate_gif.sh $names'") ================================================ FILE: gifs/demo-magic.sh ================================================ #!/usr/bin/env bash ############################################################################### # # demo-magic.sh # # Copyright (c) 2015 Paxton Hare # # This script lets you script demos in bash. It runs through your demo script when you press # ENTER. It simulates typing and runs commands. # ############################################################################### # the speed to "type" the text TYPE_SPEED=20 # no wait after "p" or "pe" NO_WAIT=false # if > 0, will pause for this amount of seconds before automatically proceeding with any p or pe PROMPT_TIMEOUT=0 # don't show command number unless user specifies it SHOW_CMD_NUMS=false # handy color vars for pretty prompts BLACK="\033[0;30m" BLUE="\033[0;34m" GREEN="\033[0;32m" GREY="\033[0;90m" CYAN="\033[0;36m" RED="\033[0;31m" PURPLE="\033[0;35m" BROWN="\033[0;33m" WHITE="\033[1;37m" COLOR_RESET="\033[0m" C_NUM=0 # prompt and command color which can be overriden DEMO_PROMPT="$ " DEMO_CMD_COLOR=$WHITE DEMO_COMMENT_COLOR=$GREY ## # prints the script usage ## function usage() { echo -e "" echo -e "Usage: $0 [options]" echo -e "" echo -e "\tWhere options is one or more of:" echo -e "\t-h\tPrints Help text" echo -e "\t-d\tDebug mode. Disables simulated typing" echo -e "\t-n\tNo wait" echo -e "\t-w\tWaits max the given amount of seconds before proceeding with demo (e.g. '-w5')" echo -e "" } ## # wait for user to press ENTER # if $PROMPT_TIMEOUT > 0 this will be used as the max time for proceeding automatically ## function wait() { if [[ "$PROMPT_TIMEOUT" == "0" ]]; then read -rs else read -rst "$PROMPT_TIMEOUT" fi } ## # print command only. Useful for when you want to pretend to run a command # # takes 1 param - the string command to print # # usage: p "ls -l" # ## function p() { if [[ ${1:0:1} == "#" ]]; then cmd=$DEMO_COMMENT_COLOR$1$COLOR_RESET else cmd=$DEMO_CMD_COLOR$1$COLOR_RESET fi # render the prompt x=$(PS1="$DEMO_PROMPT" "$BASH" --norc -i &1 | sed -n '${s/^\(.*\)exit$/\1/p;}') # show command number is selected if $SHOW_CMD_NUMS; then printf "[$((++C_NUM))] $x" else printf "$x" fi # wait for the user to press a key before typing the command if [ $NO_WAIT = false ]; then wait fi if [[ -z $TYPE_SPEED ]]; then echo -en "$cmd" else echo -en "$cmd" | pv -qL $[$TYPE_SPEED+(-2 + RANDOM%5)]; fi # wait for the user to press a key before moving on if [ $NO_WAIT = false ]; then wait fi echo "" } ## # Prints and executes a command # # takes 1 parameter - the string command to run # # usage: pe "ls -l" # ## function pe() { # print the command p "$@" run_cmd "$@" } ## # print and executes a command immediately # # takes 1 parameter - the string command to run # # usage: pei "ls -l" # ## function pei { NO_WAIT=true pe "$@" } ## # Enters script into interactive mode # # and allows newly typed commands to be executed within the script # # usage : cmd # ## function cmd() { # render the prompt x=$(PS1="$DEMO_PROMPT" "$BASH" --norc -i &1 | sed -n '${s/^\(.*\)exit$/\1/p;}') printf "$x\033[0m" read command run_cmd "${command}" } function run_cmd() { function handle_cancel() { printf "" } trap handle_cancel SIGINT stty -echoctl eval "$@" stty echoctl trap - SIGINT } function check_pv() { command -v pv >/dev/null 2>&1 || { echo "" echo -e "${RED}##############################################################" echo "# HOLD IT!! I require pv but it's not installed. Aborting." >&2; echo -e "${RED}##############################################################" echo "" echo -e "${COLOR_RESET}Installing pv:" echo "" echo -e "${BLUE}Mac:${COLOR_RESET} $ brew install pv" echo "" echo -e "${BLUE}Other:${COLOR_RESET} http://www.ivarch.com/programs/pv.shtml" echo -e "${COLOR_RESET}" exit 1; } } function updateFile(){ rm -f $1 if [ $# -eq 1 ]; then while IFS= read -r data; do echo "$data" >> $1 ; done; else echo $2 > $1 fi p "cat $1" rougify --theme tulip $1 sleep 1 } function clearConsole(){ clear } function doSleep(){ sleep $1 } check_pv # # handle some default params # -h for help # -d for disabling simulated typing # while getopts ":dhncw:" opt; do case $opt in h) usage exit 1 ;; d) unset TYPE_SPEED ;; n) NO_WAIT=true ;; c) SHOW_CMD_NUMS=true ;; w) PROMPT_TIMEOUT=$OPTARG ;; esac done ================================================ FILE: gifs/demo-no-magic.sh ================================================ # Mock of demo magic, for running on CI function p() { echo "running: $@" } function pe() { p "$@" run_cmd "$@" } function pei { NO_WAIT=true pe "$@" } function cmd() { run_cmd "${command}" } function run_cmd() { eval "$@" } function updateFile(){ rm -f $1 if [ $# -eq 1 ]; then while IFS= read -r data; do echo "$data" >> $1 ; done; else echo $2 > $1 fi p "cat $1" rougify --theme tulip $1 doSleep 1 } function clearConsole(){ echo clear } function doSleep(){ echo sleep $1 } ================================================ FILE: gifs/example.sh ================================================ #!/bin/bash ######################## # include the magic ######################## SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd) if [[ -z "${ASCIINEMA_REC}" ]]; then # Code here will be run before the recording session # Warm up scala-cli echo "println(1)" | scala-cli - # or do other preparation (e.g. create code) else . $SCRIPT_DIR/../demo-magic.sh # hide the evidence clear # Put your stuff here pe "echo 'println(\"\")' | scala-cli -" # Wait a bit to read output of last command sleep 2 echo " " && echo "ok" > status.txt fi ================================================ FILE: gifs/run_scenario.sh ================================================ #!/usr/bin/env bash set -euxo pipefail SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd) if [ "$#" != "1" ]; then echo "Please provide one scenario file!" exit 1 fi fileName=$(basename $1) name=${fileName%%.sh} script=$SCRIPT_DIR/scenarios/$name.sh ls $script #warmup $script -n echo "Done with $?" test -f status.txt && rm status.txt #do recording ( # do recording with tty tty && asciinema rec --overwrite --command="$script -n" $SCRIPT_DIR/out/$name.cast ) || ( # without tty just run the command export ASCIINEMA_REC=true && # remove magic from demo... cp $SCRIPT_DIR/demo-no-magic.sh $SCRIPT_DIR/demo-magic.sh && $script -n ) test -f status.txt || ( echo "Scenario $script failed." && echo "In case logs show that is should succeed check if it creates a status.txt file at the end" && exit 1 ) ================================================ FILE: gifs/scenarios/complete-install.sh ================================================ #!/bin/bash set -e ######################## # include the magic ######################## SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd) if [[ -z "${ASCIINEMA_REC}" ]]; then # Warm up scala-cli rm /usr/bin/scala-cli || true # remove scala-cliu from PATH rm /usr/bin/java || true # remove java from PATH # or do other preparation (e.g. create code) else . $SCRIPT_DIR/../demo-magic.sh # # hide the evidence clearConsole # Put your stuff here pe scala-cli || true pe java || true doSleep 2 pe "curl -sSLf https://scala-cli.virtuslab.org/get | sh" pe 'source ~/.profile' pe "echo 'println(\"Hello from scala-cli\")' | scala-cli -" # Wait a bit to read output of last command doSleep 2 echo " " && echo "ok" > status.txt fi ================================================ FILE: gifs/scenarios/defaults.sh ================================================ #!/bin/bash set -e ######################## # include the magic ######################## SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd) if [[ -z "${ASCIINEMA_REC}" ]]; then # Warm up scala-cli echo "println(1)" | scala-cli - scala-cli fmt . # or do other preparation (e.g. create code) else . $SCRIPT_DIR/../demo-magic.sh # # hide the evidence clearConsole # Put your stuff here cat < status.txt fi ================================================ FILE: gifs/scenarios/demo.sh ================================================ #!/bin/bash set -e ######################## # include the magic ######################## SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd) if [[ -z "${ASCIINEMA_REC}" ]]; then # Warm up scala-cli echo "println(1)" | scala-cli - cat < demo.test.scala | //> using dep org.scalameta::munit:0.7.29 EOF scala-cli test demo.test.scala # or do other preparation (e.g. create code) else . $SCRIPT_DIR/../demo-magic.sh # # hide the evidence clearConsole cat < using dep org.scalameta::munit:0.7.29 class demoTest extends munit.FunSuite { test("test nice args") { assert(clue(niceArgs("a", "b")) == "Hello: A, B!") } test("test empty arguments") { assert(clue(niceArgs()) == "Hello!") } } EOF pe "scala-cli test demo.scala demo.test.scala" || true # Wait a bit to read output of last command doSleep 5 echo " " && echo "ok" > status.txt fi ================================================ FILE: gifs/scenarios/education.sh ================================================ #!/bin/bash set -e ######################## # include the magic ######################## SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd) if [[ -z "${ASCIINEMA_REC}" ]]; then # Warm up scala-cli echo "println(1)" | scala-cli - # or do other preparation (e.g. create code) else . $SCRIPT_DIR/../demo-magic.sh # # hide the evidence clearConsole cat < status.txt fi ================================================ FILE: gifs/scenarios/embeddable_scripts.sh ================================================ #!/bin/bash ######################## # include the magic ######################## SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd) if [[ -z "${ASCIINEMA_REC}" ]]; then # Warm up scala-cli echo "println(1)" | scala-cli - # or do other preparation (e.g. create code) else . $SCRIPT_DIR/../demo-magic.sh # # hide the evidence clearConsole # Put your stuff here cat < using scala 3.0.2 import scala.io.StdIn.readLine import LazyList.continually println(continually(readLine).takeWhile(_ != null).length) EOF doSleep 2 pe "chmod +x count_lines.sc" pe 'echo -e "abc\ndef" | ./count_lines.sc' pe 'echo -e "abc\ndef\nghi" | ./count_lines.sc' doSleep 4 echo " " && echo "ok" > status.txt fi ================================================ FILE: gifs/scenarios/fast-scripts.sh ================================================ #!/bin/bash set -e ######################## # include the magic ######################## SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd) if [[ -z "${ASCIINEMA_REC}" ]]; then # Warm up scala-cli echo "println(1)" | scala-cli - # or do other preparation (e.g. create code) else . $SCRIPT_DIR/../demo-magic.sh # # hide the evidence clearConsole # Put your stuff here pe "echo 'println(\"TODO: turn gifs/scenarios/fast-scripts.sh into proper scenario showing Fast Scripts" key="fast-scripts" scripting="true\")' | scala-cli -" # Wait a bit to read output of last command doSleep 2 echo " " && echo "ok" > status.txt fi ================================================ FILE: gifs/scenarios/learning_curve.sh ================================================ #!/bin/bash set -e ######################## # include the magic ######################## SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd) if [[ -z "${ASCIINEMA_REC}" ]]; then # Warm up scala-cli echo "println(1)" | scala-cli -S 3.0.2 - # or do other preparation (e.g. create code) else . $SCRIPT_DIR/../demo-magic.sh # # hide the evidence clearConsole # Put your stuff here cat < using scala 3.0.2 @main def hello() = println("Hello world from Scala CLI") EOF pe "scala-cli Hello.scala" # Wait a bit to read output of last command doSleep 2 clearConsole cat < using scala 2.13.6 object Hello extends App { println("Hello world from Scala CLI") } EOF pe "scala-cli Hello.scala" echo " " && echo "ok" > status.txt fi ================================================ FILE: gifs/scenarios/powerful_scripts.sh ================================================ #!/bin/bash set -e ######################## # include the magic ######################## SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd) if [[ -z "${ASCIINEMA_REC}" ]]; then # Warm up scala-cli echo '//> using dep com.lihaoyi::os-lib:0.9.1' | scala-cli - echo '//> using dep com.lihaoyi::pprint:0.8.1' | scala-cli - # or do other preparation (e.g. create code) else . $SCRIPT_DIR/../demo-magic.sh # # hide the evidence clearConsole # Put your stuff here cat < using dep com.lihaoyi::os-lib:0.9.1 //> using dep com.lihaoyi::pprint:0.8.1 import pprint._ import os._ val path = Path(args(0), pwd) pprintln(os.stat(path)) EOF doSleep 3 pe "chmod +x stat.sc" pe 'echo "Hello" > my_file' pe "scala-cli ./stat.sc -- my_file" # Wait a bit to read output of last command doSleep 4 echo " " && echo "ok" > status.txt fi ================================================ FILE: gifs/scenarios/projects.sh ================================================ #!/bin/bash set -e ######################## # include the magic ######################## SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd) if [[ -z "${ASCIINEMA_REC}" ]]; then # Warm up scala-cli echo "println(1)" | scala-cli -S 2.13.6 - echo "//> using dep com.softwaremill.sttp.client3::core:3.8.13" | scala-cli -S 2.13.6 - scala-cli config suppress-warning.outdated-dependencies-files true # or do other preparation (e.g. create code) else . $SCRIPT_DIR/../demo-magic.sh # # hide the evidence clearConsole # Put your stuff here cat < using dep com.softwaremill.sttp.client3::core:3.3.18 import sttp.client3._ // https://sttp.softwaremill.com/en/latest/quickstart.html object Post extends App { val backend = HttpURLConnectionBackend() val response = basicRequest .body("Hello, world!") .post(uri"https://httpbin.org/post?hello=world").send(backend) println(response.body) } EOF doSleep 2 pe "scala-cli Post.scala -S 2.13.6" # Wait a bit to read output of last command doSleep 5 echo " " && echo "ok" > status.txt fi ================================================ FILE: gifs/scenarios/prototyping.sh ================================================ #!/bin/bash set -e ######################## # include the magic ######################## SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd) if [[ -z "${ASCIINEMA_REC}" ]]; then # Warm up scala-cli echo "println(1)" | scala-cli -S 3.0.2 - echo "println(1)" | scala-cli -S 2.13.6 - # or do other preparation (e.g. create code) else . $SCRIPT_DIR/../demo-magic.sh # # hide the evidence clearConsole # Put your stuff here cat < status.txt fi ================================================ FILE: gifs/scenarios/scripting.sh ================================================ #!/bin/bash set -e ######################## # include the magic ######################## SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd) if [[ -z "${ASCIINEMA_REC}" ]]; then # Warm up scala-cli echo "println(1)" | scala-cli - echo "//> using dep \"com.lihaoyi::os-lib::0.9.1\"" | scala-cli - # or do other preparation (e.g. create code) else . $SCRIPT_DIR/../demo-magic.sh # # hide the evidence clearConsole # Put your stuff here cat < using dep com.lihaoyi::os-lib::0.9.1 val filePath = os.pwd / "file" val fileContent = os.read(filePath) println(fileContent) EOF pe "scala-cli script.sc" doSleep 5 clearConsole cat < status.txt fi ================================================ FILE: gifs/scenarios/self-contained-examples.sh ================================================ #!/bin/bash set -e ######################## # include the magic ######################## SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd) if [[ -z "${ASCIINEMA_REC}" ]]; then # Warm up scala-cli echo "println(1)" | scala-cli - # or do other preparation (e.g. create code) else . $SCRIPT_DIR/../demo-magic.sh # # hide the evidence clearConsole # Put your stuff here pe "scala-cli https://gist.github.com/alexarchambault/7b4ec20c4033690dd750ffd601e540ec" doSleep 3 clearConsole pe "scala-cli https://gist.github.com/lwronski/99bb89d1962d2c5e21da01f1ad60e92f" || true doSleep 2 pe "scala-cli https://gist.github.com/lwronski/99bb89d1962d2c5e21da01f1ad60e92f -M ScalaCli" # Wait a bit to read output of last command doSleep 2 echo " " && echo "ok" > status.txt fi ================================================ FILE: gifs/scenarios/todo.sh ================================================ #!/bin/bash set -e ######################## # include the magic ######################## SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd) if [[ -z "${ASCIINEMA_REC}" ]]; then # Warm up scala-cli echo "println(1)" | scala-cli - else . $SCRIPT_DIR/../demo-magic.sh # # hide the evidence clearConsole # Put your stuff here pe "scala-cli version" pe "echo 'println(\"TODO\")' | scala-cli -" # Wait a bit to read output of last command doSleep 2 echo " " && echo "ok" > status.txt fi ================================================ FILE: gifs/scenarios/universal_tool.sh ================================================ #!/bin/bash set -e ######################## # include the magic ######################## SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd) if [[ -z "${ASCIINEMA_REC}" ]]; then # Warm up scala-cli echo "println(1)" | scala-cli - echo "println(1)" | scala-cli --js - && echo "println(1)" | scala-cli --native - # or do other preparation (e.g. create code) else . $SCRIPT_DIR/../demo-magic.sh # # hide the evidence clearConsole cat < status.txt fi ================================================ FILE: gifs/scenarios/versions.sh ================================================ #!/bin/bash set -e ######################## # include the magic ######################## SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd) if [[ -z "${ASCIINEMA_REC}" ]]; then # Warm up scala-cli cat < versions.scala object ScalaVersion extends App { def props(url: java.net.URL): java.util.Properties = { val properties = new java.util.Properties() val is = url.openStream() try { properties.load(is) properties } finally is.close() } def scala2Version: String = props(getClass.getResource("/library.properties")).getProperty("version.number") def checkScala3(res: java.util.Enumeration[java.net.URL]): String = if (!res.hasMoreElements) scala2Version else { val manifest = props(res.nextElement) manifest.getProperty("Specification-Title") match { case "scala3-library-bootstrapped" => manifest.getProperty("Implementation-Version") case _ => checkScala3(res) } } val manifests = getClass.getClassLoader.getResources("META-INF/MANIFEST.MF") val scalaVersion = checkScala3(manifests) val javaVersion = System.getProperty("java.version") println(s"Scala: \$scalaVersion Java: \$javaVersion") } EOF cat < classpath.scala object Main extends App { val classpath = System.getProperty("java.class.path").split(java.io.File.pathSeparator) val ignoreIf = Seq("scala-cli", "scala-lang", "jline", "scala-sbt", "pretty-stacktraces", "java/dev/jna", "protobuf-java") println(classpath.toList .filter(l => !ignoreIf.exists(l.contains)) .filter(_.endsWith(".jar")) .map(_.split("/").last) .sorted .mkString("Jars: ", ", ", "") ) } EOF scala-cli versions.scala scala-cli --scala 2.13.18 versions.scala scala-cli --scala 2.12.21 versions.scala scala-cli --jvm 17 versions.scala scala-cli --jvm zulu:21 versions.scala scala-cli --scala 2.13.18 --dep org.typelevel::cats-core:2.3.0 classpath.scala scala-cli --dep org.scalameta::munit:0.7.29 classpath.scala else . $SCRIPT_DIR/../demo-magic.sh # # hide the evidence clearConsole # Put your stuff here pe "scala-cli versions.scala" pe "scala-cli --scala 2.13.18 versions.scala" pe "scala-cli --scala 2.12.21 versions.scala" doSleep 2 clearConsole pe "scala-cli --jvm 17 versions.scala" pe "scala-cli --jvm zulu:21 versions.scala" doSleep 2 clearConsole pe "scala-cli --dep org.scalameta::munit:0.7.29 classpath.scala" pe "scala-cli --scala 2.13.18 --dep org.typelevel::cats-core:2.3.0 classpath.scala" # Wait a bit to read output of last command doSleep 2 echo " " && echo "ok" > status.txt fi ================================================ FILE: gifs/svg_render/Dockerfile ================================================ FROM node:12.18.1 RUN npm install -g svg-term-cli RUN mkdir /profiles # Terminal themes for light and dark mode of the site # Based on https://iterm2colorschemes.com/ RUN curl -fLo /profiles/dark https://raw.githubusercontent.com/mbadolato/iTerm2-Color-Schemes/master/schemes/Chester.itermcolors RUN curl -fLo /profiles/light https://raw.githubusercontent.com/mbadolato/iTerm2-Color-Schemes/master/schemes/Jackie%20Brown.itermcolors ENTRYPOINT svg-term $* ================================================ FILE: gifs/svg_render/README.md ================================================ This is docker image that is used to generate our scripts. The image is based on amazing svg-term-cli project by marionebl: https://github.com/marionebl/svg-term-cli The themes are based on https://iterm2colorschemes.com/. ================================================ FILE: mill ================================================ #!/usr/bin/env bash # Adapted from coursier_version="2.1.25-M24" COMMAND=$@ # necessary for Windows various shell environments IS_WINDOWS=$(uname | grep -E 'CYG*|MSYS*|MING*|UCRT*|ClANG*|GIT*') # https://stackoverflow.com/questions/3466166/how-to-check-if-running-in-cygwin-mac-or-linux/17072017#17072017 if [ "$(expr substr $(uname -s) 1 5 2>/dev/null)" == "Linux" ]; then if [ "$(uname -m)" == "aarch64" ]; then cs_url="https://github.com/coursier/coursier/releases/download/v$coursier_version/cs-aarch64-pc-linux.gz" else cs_url="https://github.com/coursier/coursier/releases/download/v$coursier_version/cs-x86_64-pc-linux.gz" fi cache_base="$HOME/.cache/coursier/v1" elif [ "$(uname)" == "Darwin" ]; then if [ "$(uname -p)" == "arm" ]; then cs_url="https://github.com/coursier/coursier/releases/download/v$coursier_version/cs-aarch64-apple-darwin.gz" else cs_url="https://github.com/coursier/coursier/releases/download/v$coursier_version/cs-x86_64-apple-darwin.gz" fi cache_base="$HOME/Library/Caches/Coursier/v1" else # assuming Windows… cs_url="https://github.com/coursier/coursier/releases/download/v$coursier_version/cs-x86_64-pc-win32.zip" cache_base="$LOCALAPPDATA/Coursier/v1" # TODO Check that ext=".exe" do_chmod="0" fi cache_dest="$cache_base/$(echo "$cs_url" | sed 's@://@/@')" if [ ! -f "$cache_dest" ]; then mkdir -p "$(dirname "$cache_dest")" tmp_dest="$cache_dest.tmp-setup" echo "Downloading $cs_url" curl -fLo "$tmp_dest" "$cs_url" mv "$tmp_dest" "$cache_dest" fi if [[ "$cache_dest" == *.gz ]]; then cs="$(dirname "$cache_dest")/$(basename "$cache_dest" .gz)" if [ ! -f "$cs" ]; then gzip -d < "$cache_dest" > "$cs" fi if [ ! -x "$cs" ]; then chmod +x "$cs" fi elif [[ "$cache_dest" == *.zip ]]; then cs="$(dirname "$cache_dest")/$(basename "$cache_dest" .zip).exe" if [ ! -f "$cs" ]; then unzip -p "$cache_dest" "$(basename "$cache_dest" .zip).exe" > "$cs" fi fi # If cs is not a valid file, fall back to the `cs` command in PATH if [ ! -f "$cs" ]; then cs="$(command -v cs)" fi function to_bash_syntax { local S for S in "$@" ; do echo "$S" | sed -E -e 's#^set #export #' -e 's#%([A-Z_][A-Z_0-9]*)%#${\1}#g' | tr '\\' '/' done } if [[ $IS_WINDOWS ]]; then # needed for coursier version < 2.1.8, harmless otherwise IFS=$'\n' # temurin:17 (build 17+35) doesn't support utf8 filenames, although some later 17 versions do eval "$(to_bash_syntax `"$cs" java --env --jvm zulu:17` || to_bash_syntax `"$cs" java --env --jvm openjdk:1.17.0`)" unset IFS else eval "$("$cs" java --env --jvm temurin:17 || "$cs" java --env --jvm openjdk:1.17.0)" fi # temporary, until we pass JPMS options to native-image, # see https://www.graalvm.org/release-notes/22_2/#native-image export USE_NATIVE_IMAGE_JAVA_PLATFORM_MODULE_SYSTEM=false DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd)" if [[ $IS_WINDOWS ]]; then exec "$DIR/mill.bat" "$@" else exec "$DIR/millw" $COMMAND fi ================================================ FILE: mill.bat ================================================ @echo off setlocal enabledelayedexpansion if [!DEFAULT_MILL_VERSION!]==[] ( set "DEFAULT_MILL_VERSION=1.1.5" ) if [!MILL_GITHUB_RELEASE_CDN!]==[] ( set "MILL_GITHUB_RELEASE_CDN=" ) if [!MILL_MAIN_CLI!]==[] ( set "MILL_MAIN_CLI=%~f0" ) set "MILL_REPO_URL=https://github.com/com-lihaoyi/mill" SET MILL_BUILD_SCRIPT= if exist "build.mill" ( set MILL_BUILD_SCRIPT=build.mill ) else ( if exist "build.mill.scala" ( set MILL_BUILD_SCRIPT=build.mill.scala ) else ( if exist "build.sc" ( set MILL_BUILD_SCRIPT=build.sc ) else ( rem no-op ) ) ) if [!MILL_VERSION!]==[] ( if exist .mill-version ( set /p MILL_VERSION=<.mill-version ) else ( if exist .config\mill-version ( set /p MILL_VERSION=<.config\mill-version ) else ( rem Determine which config file to use for version extraction set "MILL_VERSION_CONFIG_FILE=" set "MILL_VERSION_SEARCH_PATTERN=" if exist build.mill.yaml ( set "MILL_VERSION_CONFIG_FILE=build.mill.yaml" set "MILL_VERSION_SEARCH_PATTERN=mill-version:" ) else ( if not "%MILL_BUILD_SCRIPT%"=="" ( set "MILL_VERSION_CONFIG_FILE=%MILL_BUILD_SCRIPT%" set "MILL_VERSION_SEARCH_PATTERN=//\|.*mill-version" ) ) rem Process the config file if found if not "!MILL_VERSION_CONFIG_FILE!"=="" ( rem Find the line and process it for /f "tokens=*" %%a in ('findstr /R /C:"!MILL_VERSION_SEARCH_PATTERN!" "!MILL_VERSION_CONFIG_FILE!"') do ( set "line=%%a" rem --- 1. Replicate sed 's/.*://' --- rem This removes everything up to and including the first colon set "line=!line:*:=!" rem --- 2. Replicate sed 's/#.*//' --- rem Split on '#' and keep the first part for /f "tokens=1 delims=#" %%b in ("!line!") do ( set "line=%%b" ) rem --- 3. Replicate sed 's/['"]//g' --- rem Remove all quotes set "line=!line:'=!" set "line=!line:"=!" rem --- 4. Replicate sed's trim/space removal --- rem Remove all space characters from the result. This is more robust. set "MILL_VERSION=!line: =!" rem We found the version, so we can exit the loop goto :version_found ) :version_found rem no-op ) ) ) ) if [!MILL_VERSION!]==[] ( set MILL_VERSION=%DEFAULT_MILL_VERSION% ) if [!MILL_FINAL_DOWNLOAD_FOLDER!]==[] set MILL_FINAL_DOWNLOAD_FOLDER=%USERPROFILE%\.cache\mill\download rem without bat file extension, cmd doesn't seem to be able to run it set "MILL_NATIVE_SUFFIX=-native" set "MILL_JVM_SUFFIX=-jvm" set "MILL_FULL_VERSION=%MILL_VERSION%" set "MILL_DOWNLOAD_EXT=.bat" set "ARTIFACT_SUFFIX=" REM Check if MILL_VERSION contains MILL_NATIVE_SUFFIX echo !MILL_VERSION! | findstr /C:"%MILL_NATIVE_SUFFIX%" >nul if !errorlevel! equ 0 ( set "MILL_VERSION=%MILL_VERSION:-native=%" REM -native images compiled with graal do not support windows-arm REM https://github.com/oracle/graal/issues/9215 IF /I NOT "%PROCESSOR_ARCHITECTURE%"=="ARM64" ( set "ARTIFACT_SUFFIX=-native-windows-amd64" set "MILL_DOWNLOAD_EXT=.exe" ) else ( rem no-op ) ) else ( echo !MILL_VERSION! | findstr /C:"%MILL_JVM_SUFFIX%" >nul if !errorlevel! equ 0 ( set "MILL_VERSION=%MILL_VERSION:-jvm=%" ) else ( set "SKIP_VERSION=false" set "MILL_PREFIX=%MILL_VERSION:~0,4%" if "!MILL_PREFIX!"=="0.1." set "SKIP_VERSION=true" if "!MILL_PREFIX!"=="0.2." set "SKIP_VERSION=true" if "!MILL_PREFIX!"=="0.3." set "SKIP_VERSION=true" if "!MILL_PREFIX!"=="0.4." set "SKIP_VERSION=true" if "!MILL_PREFIX!"=="0.5." set "SKIP_VERSION=true" if "!MILL_PREFIX!"=="0.6." set "SKIP_VERSION=true" if "!MILL_PREFIX!"=="0.7." set "SKIP_VERSION=true" if "!MILL_PREFIX!"=="0.8." set "SKIP_VERSION=true" if "!MILL_PREFIX!"=="0.9." set "SKIP_VERSION=true" set "MILL_PREFIX=%MILL_VERSION:~0,5%" if "!MILL_PREFIX!"=="0.10." set "SKIP_VERSION=true" if "!MILL_PREFIX!"=="0.11." set "SKIP_VERSION=true" if "!MILL_PREFIX!"=="0.12." set "SKIP_VERSION=true" if "!SKIP_VERSION!"=="false" ( IF /I NOT "%PROCESSOR_ARCHITECTURE%"=="ARM64" ( set "ARTIFACT_SUFFIX=-native-windows-amd64" set "MILL_DOWNLOAD_EXT=.exe" ) ) else ( rem no-op ) ) ) set MILL=%MILL_FINAL_DOWNLOAD_FOLDER%\!MILL_FULL_VERSION!!MILL_DOWNLOAD_EXT! set MILL_RESOLVE_DOWNLOAD= if not exist "%MILL%" ( set MILL_RESOLVE_DOWNLOAD=true ) else ( if defined MILL_TEST_DRY_RUN_LAUNCHER_SCRIPT ( set MILL_RESOLVE_DOWNLOAD=true ) else ( rem no-op ) ) if [!MILL_RESOLVE_DOWNLOAD!]==[true] ( set MILL_VERSION_PREFIX=%MILL_VERSION:~0,4% set MILL_SHORT_VERSION_PREFIX=%MILL_VERSION:~0,2% rem Since 0.5.0 set MILL_DOWNLOAD_SUFFIX=-assembly rem Since 0.11.0 set MILL_DOWNLOAD_FROM_MAVEN=1 if [!MILL_VERSION_PREFIX!]==[0.0.] ( set MILL_DOWNLOAD_SUFFIX= set MILL_DOWNLOAD_FROM_MAVEN=0 ) if [!MILL_VERSION_PREFIX!]==[0.1.] ( set MILL_DOWNLOAD_SUFFIX= set MILL_DOWNLOAD_FROM_MAVEN=0 ) if [!MILL_VERSION_PREFIX!]==[0.2.] ( set MILL_DOWNLOAD_SUFFIX= set MILL_DOWNLOAD_FROM_MAVEN=0 ) if [!MILL_VERSION_PREFIX!]==[0.3.] ( set MILL_DOWNLOAD_SUFFIX= set MILL_DOWNLOAD_FROM_MAVEN=0 ) if [!MILL_VERSION_PREFIX!]==[0.4.] ( set MILL_DOWNLOAD_SUFFIX= set MILL_DOWNLOAD_FROM_MAVEN=0 ) if [!MILL_VERSION_PREFIX!]==[0.5.] set MILL_DOWNLOAD_FROM_MAVEN=0 if [!MILL_VERSION_PREFIX!]==[0.6.] set MILL_DOWNLOAD_FROM_MAVEN=0 if [!MILL_VERSION_PREFIX!]==[0.7.] set MILL_DOWNLOAD_FROM_MAVEN=0 if [!MILL_VERSION_PREFIX!]==[0.8.] set MILL_DOWNLOAD_FROM_MAVEN=0 if [!MILL_VERSION_PREFIX!]==[0.9.] set MILL_DOWNLOAD_FROM_MAVEN=0 set MILL_VERSION_PREFIX=%MILL_VERSION:~0,5% if [!MILL_VERSION_PREFIX!]==[0.10.] set MILL_DOWNLOAD_FROM_MAVEN=0 set MILL_VERSION_PREFIX=%MILL_VERSION:~0,8% if [!MILL_VERSION_PREFIX!]==[0.11.0-M] set MILL_DOWNLOAD_FROM_MAVEN=0 set MILL_VERSION_PREFIX=%MILL_VERSION:~0,5% set DOWNLOAD_EXT=exe if [!MILL_SHORT_VERSION_PREFIX!]==[0.] set DOWNLOAD_EXT=jar if [!MILL_VERSION_PREFIX!]==[0.12.] set DOWNLOAD_EXT=exe if [!MILL_VERSION!]==[0.12.0] set DOWNLOAD_EXT=jar if [!MILL_VERSION!]==[0.12.1] set DOWNLOAD_EXT=jar if [!MILL_VERSION!]==[0.12.2] set DOWNLOAD_EXT=jar if [!MILL_VERSION!]==[0.12.3] set DOWNLOAD_EXT=jar if [!MILL_VERSION!]==[0.12.4] set DOWNLOAD_EXT=jar if [!MILL_VERSION!]==[0.12.5] set DOWNLOAD_EXT=jar if [!MILL_VERSION!]==[0.12.6] set DOWNLOAD_EXT=jar if [!MILL_VERSION!]==[0.12.7] set DOWNLOAD_EXT=jar if [!MILL_VERSION!]==[0.12.8] set DOWNLOAD_EXT=jar if [!MILL_VERSION!]==[0.12.9] set DOWNLOAD_EXT=jar if [!MILL_VERSION!]==[0.12.10] set DOWNLOAD_EXT=jar if [!MILL_VERSION!]==[0.12.11] set DOWNLOAD_EXT=jar set MILL_VERSION_PREFIX= set MILL_SHORT_VERSION_PREFIX= for /F "delims=- tokens=1" %%A in ("!MILL_VERSION!") do set MILL_VERSION_BASE=%%A set MILL_VERSION_MILESTONE= for /F "delims=- tokens=2" %%A in ("!MILL_VERSION!") do set MILL_VERSION_MILESTONE=%%A set MILL_VERSION_MILESTONE_START=!MILL_VERSION_MILESTONE:~0,1! if [!MILL_VERSION_MILESTONE_START!]==[M] ( set MILL_VERSION_TAG=!MILL_VERSION_BASE!-!MILL_VERSION_MILESTONE! ) else ( set MILL_VERSION_TAG=!MILL_VERSION_BASE! ) if [!MILL_DOWNLOAD_FROM_MAVEN!]==[1] ( set MILL_DOWNLOAD_URL=https://repo1.maven.org/maven2/com/lihaoyi/mill-dist!ARTIFACT_SUFFIX!/!MILL_VERSION!/mill-dist!ARTIFACT_SUFFIX!-!MILL_VERSION!.!DOWNLOAD_EXT! ) else ( set MILL_DOWNLOAD_URL=!MILL_GITHUB_RELEASE_CDN!%MILL_REPO_URL%/releases/download/!MILL_VERSION_TAG!/!MILL_VERSION!!MILL_DOWNLOAD_SUFFIX! ) if defined MILL_TEST_DRY_RUN_LAUNCHER_SCRIPT ( echo !MILL_DOWNLOAD_URL! echo !MILL! exit /b 0 ) rem there seems to be no way to generate a unique temporary file path (on native Windows) if defined MILL_OUTPUT_DIR ( set MILL_TEMP_DOWNLOAD_FILE=%MILL_OUTPUT_DIR%\mill-temp-download if not exist "%MILL_OUTPUT_DIR%" mkdir "%MILL_OUTPUT_DIR%" ) else ( set MILL_TEMP_DOWNLOAD_FILE=out\mill-bootstrap-download if not exist "out" mkdir "out" ) echo Downloading mill !MILL_VERSION! from !MILL_DOWNLOAD_URL! ... 1>&2 curl -f -L "!MILL_DOWNLOAD_URL!" -o "!MILL_TEMP_DOWNLOAD_FILE!" if not exist "%MILL_FINAL_DOWNLOAD_FOLDER%" mkdir "%MILL_FINAL_DOWNLOAD_FOLDER%" move /y "!MILL_TEMP_DOWNLOAD_FILE!" "%MILL%" set MILL_TEMP_DOWNLOAD_FILE= set MILL_DOWNLOAD_SUFFIX= ) set MILL_FINAL_DOWNLOAD_FOLDER= set MILL_VERSION= set MILL_REPO_URL= rem Need to preserve the first position of those listed options set MILL_FIRST_ARG= if [%~1%]==[--bsp] ( set MILL_FIRST_ARG=%1% ) else ( if [%~1%]==[-i] ( set MILL_FIRST_ARG=%1% ) else ( if [%~1%]==[--interactive] ( set MILL_FIRST_ARG=%1% ) else ( if [%~1%]==[--no-server] ( set MILL_FIRST_ARG=%1% ) else ( if [%~1%]==[--no-daemon] ( set MILL_FIRST_ARG=%1% ) else ( if [%~1%]==[--help] ( set MILL_FIRST_ARG=%1% ) ) ) ) ) ) set "MILL_PARAMS=%*%" if not [!MILL_FIRST_ARG!]==[] ( for /f "tokens=1*" %%a in ("%*") do ( set "MILL_PARAMS=%%b" ) ) rem -D mill.main.cli is for compatibility with Mill 0.10.9 - 0.13.0-M2 "%MILL%" %MILL_FIRST_ARG% -D "mill.main.cli=%MILL_MAIN_CLI%" %MILL_PARAMS% ================================================ FILE: millw ================================================ #!/usr/bin/env sh set -e if [ -z "${DEFAULT_MILL_VERSION}" ] ; then DEFAULT_MILL_VERSION="1.1.5"; fi if [ -z "${GITHUB_RELEASE_CDN}" ] ; then GITHUB_RELEASE_CDN=""; fi if [ -z "$MILL_MAIN_CLI" ] ; then MILL_MAIN_CLI="${0}"; fi MILL_REPO_URL="https://github.com/com-lihaoyi/mill" MILL_BUILD_SCRIPT="" if [ -f "build.mill" ] ; then MILL_BUILD_SCRIPT="build.mill" elif [ -f "build.mill.scala" ] ; then MILL_BUILD_SCRIPT="build.mill.scala" elif [ -f "build.sc" ] ; then MILL_BUILD_SCRIPT="build.sc" fi # `s/.*://`: # This is a greedy match that removes everything from the beginning of the line up to (and including) the last # colon (:). This effectively isolates the value part of the declaration. # # `s/#.*//`: # This removes any comments at the end of the line. # # `s/['\"]//g`: # This removes all single and double quotes from the string, wherever they appear (g is for "global"). # # `s/^[[:space:]]*//; s/[[:space:]]*$//`: # These two expressions trim any leading or trailing whitespace ([[:space:]] matches spaces and tabs). TRIM_VALUE_SED="s/.*://; s/#.*//; s/['\"]//g; s/^[[:space:]]*//; s/[[:space:]]*$//" if [ -z "${MILL_VERSION}" ] ; then if [ -f ".mill-version" ] ; then MILL_VERSION="$(tr '\r' '\n' < .mill-version | head -n 1 2> /dev/null)" elif [ -f ".config/mill-version" ] ; then MILL_VERSION="$(tr '\r' '\n' < .config/mill-version | head -n 1 2> /dev/null)" elif [ -f "build.mill.yaml" ] ; then MILL_VERSION="$(grep -E "mill-version:" "build.mill.yaml" | sed -E "$TRIM_VALUE_SED")" elif [ -n "${MILL_BUILD_SCRIPT}" ] ; then MILL_VERSION="$(grep -E "//\|.*mill-version" "${MILL_BUILD_SCRIPT}" | sed -E "$TRIM_VALUE_SED")" fi fi if [ -z "${MILL_VERSION}" ] ; then MILL_VERSION="${DEFAULT_MILL_VERSION}"; fi MILL_USER_CACHE_DIR="${XDG_CACHE_HOME:-${HOME}/.cache}/mill" if [ -z "${MILL_FINAL_DOWNLOAD_FOLDER}" ] ; then MILL_FINAL_DOWNLOAD_FOLDER="${MILL_USER_CACHE_DIR}/download"; fi MILL_NATIVE_SUFFIX="-native" MILL_JVM_SUFFIX="-jvm" ARTIFACT_SUFFIX="" # Check if GLIBC version is at least the required version # Returns 0 (true) if GLIBC >= required version, 1 (false) otherwise check_glibc_version() { required_version="2.39" required_major=$(echo "$required_version" | cut -d. -f1) required_minor=$(echo "$required_version" | cut -d. -f2) # Get GLIBC version from ldd --version (first line contains version like "ldd (GNU libc) 2.31") glibc_version=$(ldd --version 2>/dev/null | head -n 1 | grep -oE '[0-9]+\.[0-9]+$' || echo "") if [ -z "$glibc_version" ]; then # If we can't determine GLIBC version, assume it's too old return 1 fi glibc_major=$(echo "$glibc_version" | cut -d. -f1) glibc_minor=$(echo "$glibc_version" | cut -d. -f2) if [ "$glibc_major" -gt "$required_major" ]; then return 0 elif [ "$glibc_major" -eq "$required_major" ] && [ "$glibc_minor" -ge "$required_minor" ]; then return 0 else return 1 fi } set_artifact_suffix() { if [ "$(uname -s 2>/dev/null | cut -c 1-5)" = "Linux" ]; then # Native binaries require new enough GLIBC; fall back to JVM launcher if older if ! check_glibc_version; then return fi if [ "$(uname -m)" = "aarch64" ]; then ARTIFACT_SUFFIX="-native-linux-aarch64" else ARTIFACT_SUFFIX="-native-linux-amd64"; fi elif [ "$(uname)" = "Darwin" ]; then if [ "$(uname -m)" = "arm64" ]; then ARTIFACT_SUFFIX="-native-mac-aarch64" else ARTIFACT_SUFFIX="-native-mac-amd64"; fi else echo "This native mill launcher supports only Linux and macOS." 1>&2 exit 1 fi } case "$MILL_VERSION" in *"$MILL_NATIVE_SUFFIX") MILL_VERSION=${MILL_VERSION%"$MILL_NATIVE_SUFFIX"} set_artifact_suffix ;; *"$MILL_JVM_SUFFIX") MILL_VERSION=${MILL_VERSION%"$MILL_JVM_SUFFIX"} ;; *) case "$MILL_VERSION" in 0.1.* | 0.2.* | 0.3.* | 0.4.* | 0.5.* | 0.6.* | 0.7.* | 0.8.* | 0.9.* | 0.10.* | 0.11.* | 0.12.*) ;; *) set_artifact_suffix ;; esac ;; esac MILL="${MILL_FINAL_DOWNLOAD_FOLDER}/$MILL_VERSION$ARTIFACT_SUFFIX" # If not already downloaded, download it if [ ! -s "${MILL}" ] || [ "$MILL_TEST_DRY_RUN_LAUNCHER_SCRIPT" = "1" ] ; then case $MILL_VERSION in 0.0.* | 0.1.* | 0.2.* | 0.3.* | 0.4.*) MILL_DOWNLOAD_SUFFIX="" MILL_DOWNLOAD_FROM_MAVEN=0 ;; 0.5.* | 0.6.* | 0.7.* | 0.8.* | 0.9.* | 0.10.* | 0.11.0-M*) MILL_DOWNLOAD_SUFFIX="-assembly" MILL_DOWNLOAD_FROM_MAVEN=0 ;; *) MILL_DOWNLOAD_SUFFIX="-assembly" MILL_DOWNLOAD_FROM_MAVEN=1 ;; esac case $MILL_VERSION in 0.12.0 | 0.12.1 | 0.12.2 | 0.12.3 | 0.12.4 | 0.12.5 | 0.12.6 | 0.12.7 | 0.12.8 | 0.12.9 | 0.12.10 | 0.12.11) MILL_DOWNLOAD_EXT="jar" ;; 0.12.*) MILL_DOWNLOAD_EXT="exe" ;; 0.*) MILL_DOWNLOAD_EXT="jar" ;; *) MILL_DOWNLOAD_EXT="exe" ;; esac MILL_TEMP_DOWNLOAD_FILE="${MILL_OUTPUT_DIR:-out}/mill-temp-download" mkdir -p "$(dirname "${MILL_TEMP_DOWNLOAD_FILE}")" if [ "$MILL_DOWNLOAD_FROM_MAVEN" = "1" ] ; then MILL_DOWNLOAD_URL="https://repo1.maven.org/maven2/com/lihaoyi/mill-dist${ARTIFACT_SUFFIX}/${MILL_VERSION}/mill-dist${ARTIFACT_SUFFIX}-${MILL_VERSION}.${MILL_DOWNLOAD_EXT}" else MILL_VERSION_TAG=$(echo "$MILL_VERSION" | sed -E 's/([^-]+)(-M[0-9]+)?(-.*)?/\1\2/') MILL_DOWNLOAD_URL="${GITHUB_RELEASE_CDN}${MILL_REPO_URL}/releases/download/${MILL_VERSION_TAG}/${MILL_VERSION}${MILL_DOWNLOAD_SUFFIX}" unset MILL_VERSION_TAG fi if [ "$MILL_TEST_DRY_RUN_LAUNCHER_SCRIPT" = "1" ] ; then echo "$MILL_DOWNLOAD_URL" echo "$MILL" exit 0 fi echo "Downloading mill ${MILL_VERSION} from ${MILL_DOWNLOAD_URL} ..." 1>&2 curl -f -L -o "${MILL_TEMP_DOWNLOAD_FILE}" "${MILL_DOWNLOAD_URL}" chmod +x "${MILL_TEMP_DOWNLOAD_FILE}" mkdir -p "${MILL_FINAL_DOWNLOAD_FOLDER}" mv "${MILL_TEMP_DOWNLOAD_FILE}" "${MILL}" unset MILL_TEMP_DOWNLOAD_FILE unset MILL_DOWNLOAD_SUFFIX fi MILL_FIRST_ARG="" if [ "$1" = "--bsp" ] || [ "${1#"-i"}" != "$1" ] || [ "$1" = "--interactive" ] || [ "$1" = "--no-server" ] || [ "$1" = "--no-daemon" ] || [ "$1" = "--help" ] ; then # Need to preserve the first position of those listed options MILL_FIRST_ARG=$1 shift fi unset MILL_FINAL_DOWNLOAD_FOLDER unset MILL_OLD_DOWNLOAD_PATH unset OLD_MILL unset MILL_VERSION unset MILL_REPO_URL # -D mill.main.cli is for compatibility with Mill 0.10.9 - 0.13.0-M2 # We don't quote MILL_FIRST_ARG on purpose, so we can expand the empty value without quotes # shellcheck disable=SC2086 exec "${MILL}" $MILL_FIRST_ARG -D "mill.main.cli=${MILL_MAIN_CLI}" "$@" ================================================ FILE: modules/build/src/main/java/scala/build/internal/Chdir.java ================================================ package scala.build.internal; import coursier.exec.ErrnoException; public final class Chdir { public static boolean available() { return false; } public static void chdir(String path) throws ErrnoException { // Not supported on the JVM, returning immediately } } ================================================ FILE: modules/build/src/main/java/scala/build/internal/ChdirGraalvm.java ================================================ package scala.build.internal; import java.io.FileNotFoundException; import com.oracle.svm.core.annotate.Substitute; import com.oracle.svm.core.annotate.TargetClass; import com.oracle.svm.core.headers.LibC; import coursier.exec.ErrnoException; import coursier.exec.GraalvmErrnoExtras; import org.graalvm.nativeimage.c.type.CTypeConversion; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; @TargetClass(className = "scala.build.internal.Chdir") @Platforms({Platform.LINUX.class, Platform.DARWIN.class}) final class ChdirGraalvm { @Substitute public static boolean available() { return true; } @Substitute public static void chdir(String path) throws ErrnoException { CTypeConversion.CCharPointerHolder path0 = CTypeConversion.toCString(path); int ret = GraalvmUnistdExtras.chdir(path0.get()); if (ret != 0) { int n = LibC.errno(); Throwable cause = null; if (n == GraalvmErrnoExtras.ENOENT() || n == GraalvmErrnoExtras.ENOTDIR()) cause = new FileNotFoundException(path); throw new ErrnoException(n, cause); } } } ================================================ FILE: modules/build/src/main/java/scala/build/internal/GraalvmUnistdExtras.java ================================================ package scala.build.internal; import com.oracle.svm.core.posix.headers.PosixDirectives; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; import org.graalvm.nativeimage.c.CContext; import org.graalvm.nativeimage.c.function.CFunction; import org.graalvm.nativeimage.c.type.CCharPointer; import org.graalvm.nativeimage.c.type.CCharPointerPointer; @CContext(PosixDirectives.class) @Platforms({Platform.LINUX.class, Platform.DARWIN.class}) public class GraalvmUnistdExtras { @CFunction public static native int chdir(CCharPointer path); } ================================================ FILE: modules/build/src/main/java/scala/build/internal/JavaParserProxyMakerSubst.java ================================================ package scala.build.internal; import com.oracle.svm.core.annotate.Substitute; import com.oracle.svm.core.annotate.TargetClass; import java.util.function.Supplier; /** * This makes [[JavaParserProxyMaker.get]] provide a [[JavaParserProxyBinary]] * rather than a [[JavaParserProxyJvm]], from native launchers. * * See [[JavaParserProxyMaker]] for more details. */ @TargetClass(className = "scala.build.internal.JavaParserProxyMaker") public final class JavaParserProxyMakerSubst { @Substitute public JavaParserProxy get( Object archiveCache, scala.Option javaClassNameVersionOpt, scala.build.Logger logger, Supplier javaCommand ) { return new JavaParserProxyBinary(archiveCache, logger, javaClassNameVersionOpt, javaCommand); } } ================================================ FILE: modules/build/src/main/scala/scala/build/Bloop.scala ================================================ package scala.build import bloop.rifle.{BloopRifleConfig, BuildServer} import ch.epfl.scala.bsp4j import coursier.cache.FileCache import coursier.util.Task import dependency.parser.ModuleParser import dependency.{AnyDependency, DependencyLike, ScalaParameters, ScalaVersion} import java.io.{File, IOException} import scala.annotation.tailrec import scala.build.EitherCps.{either, value} import scala.build.errors.{BuildException, ModuleFormatError} import scala.build.internal.CsLoggerUtil.* import scala.concurrent.ExecutionException import scala.concurrent.duration.FiniteDuration import scala.jdk.CollectionConverters.* object Bloop { private object BrokenPipeInCauses { @tailrec def unapply(ex: Throwable): Option[IOException] = ex match { case null => None case ex: IOException if ex.getMessage == "Broken pipe" => Some(ex) case ex: IOException if ex.getMessage == "Connection reset by peer" => Some(ex) case _ => unapply(ex.getCause) } } def compile( projectName: String, buildServer: BuildServer, logger: Logger, buildTargetsTimeout: FiniteDuration ): Either[Throwable, Boolean] = try retry()(logger) { logger.debug("Listing BSP build targets") val results = buildServer.workspaceBuildTargets() .get(buildTargetsTimeout.length, buildTargetsTimeout.unit) val buildTargetOpt = results.getTargets.asScala.find(_.getDisplayName == projectName) val buildTarget = buildTargetOpt.getOrElse { throw new Exception( s"Expected to find project '$projectName' in build targets (only got ${results.getTargets .asScala.map("'" + _.getDisplayName + "'").mkString(", ")})" ) } logger.debug(s"Compiling $projectName with Bloop") val compileRes = buildServer.buildTargetCompile( new bsp4j.CompileParams(List(buildTarget.getId).asJava) ).get() val success = compileRes.getStatusCode == bsp4j.StatusCode.OK logger.debug(if (success) "Compilation succeeded" else "Compilation failed") Right(success) } catch { case ex @ BrokenPipeInCauses(_) => logger.debug(s"Caught $ex while exchanging with Bloop server, assuming Bloop server exited") Left(ex) case ex: ExecutionException => logger.debug( s"Caught $ex while exchanging with Bloop server, you may consider restarting the build server" ) Left(ex) } def bloopClassPath( dep: AnyDependency, params: ScalaParameters, logger: Logger, cache: FileCache[Task] ): Either[BuildException, Seq[File]] = either { val res = value { Artifacts.artifacts( Seq(Positioned.none(dep)), Seq( coursier.Repositories.centralMavenSnapshots, RepositoryUtils.snapshotsRepository, RepositoryUtils.scala3NightlyRepository ), Some(params), logger, cache.withMessage(s"Downloading compilation server ${dep.version}") ) } res.map(_._2.toIO) } def bloopClassPath( logger: Logger, cache: FileCache[Task] ): Either[BuildException, Seq[File]] = bloopClassPath(logger, cache, BloopRifleConfig.defaultVersion) def bloopClassPath( logger: Logger, cache: FileCache[Task], bloopVersion: String ): Either[BuildException, Seq[File]] = either { val moduleStr = BloopRifleConfig.defaultModule val mod = value { ModuleParser.parse(moduleStr) .left.map(err => new ModuleFormatError(moduleStr, err, Some("Bloop"))) } val dep = DependencyLike(mod, bloopVersion) val sv = BloopRifleConfig.defaultScalaVersion val sbv = ScalaVersion.binary(sv) val params = ScalaParameters(sv, sbv) value(bloopClassPath(dep, params, logger, cache)) } } ================================================ FILE: modules/build/src/main/scala/scala/build/BloopBuildClient.scala ================================================ package scala.build import ch.epfl.scala.bsp4j import scala.build.options.Scope trait BloopBuildClient extends bsp4j.BuildClient { def setProjectParams(newParams: Seq[String]): Unit def setGeneratedSources(scope: Scope, newGeneratedSources: Seq[GeneratedSource]): Unit def diagnostics: Option[Seq[(Either[String, os.Path], bsp4j.Diagnostic)]] def clear(): Unit } object BloopBuildClient { def create( logger: Logger, keepDiagnostics: Boolean ): BloopBuildClient = new ConsoleBloopBuildClient( logger, keepDiagnostics ) } ================================================ FILE: modules/build/src/main/scala/scala/build/Build.scala ================================================ package scala.build import ch.epfl.scala.bsp4j import com.swoval.files.FileTreeViews.Observer import com.swoval.files.{PathWatcher, PathWatchers} import dependency.ScalaParameters import java.io.File import java.nio.file.FileSystemException import java.util.concurrent.{ScheduledExecutorService, ScheduledFuture} import scala.annotation.tailrec import scala.build.EitherCps.{either, value} import scala.build.Ops.* import scala.build.compiler.{ScalaCompiler, ScalaCompilerMaker} import scala.build.errors.* import scala.build.input.* import scala.build.internal.resource.ResourceMapper import scala.build.internal.{Constants, MainClass, Name, Util} import scala.build.internals.ConsoleUtils.ScalaCliConsole.warnPrefix import scala.build.options.* import scala.build.options.validation.ValidationException import scala.build.postprocessing.* import scala.build.postprocessing.LineConversion.scalaLineToScLineShift import scala.collection.mutable.ListBuffer import scala.compiletime.uninitialized import scala.concurrent.duration.DurationInt import scala.util.Properties import scala.util.control.NonFatal trait Build { def inputs: Inputs def options: BuildOptions def scope: Scope def outputOpt: Option[os.Path] def success: Boolean def cancelled: Boolean def diagnostics: Option[Seq[(Either[String, os.Path], bsp4j.Diagnostic)]] def successfulOpt: Option[Build.Successful] } object Build { final case class Successful( inputs: Inputs, options: BuildOptions, scalaParams: Option[ScalaParameters], scope: Scope, sources: Sources, artifacts: Artifacts, project: Project, output: os.Path, diagnostics: Option[Seq[(Either[String, os.Path], bsp4j.Diagnostic)]], generatedSources: Seq[GeneratedSource], isPartial: Boolean, logger: Logger ) extends Build { def success: Boolean = true def cancelled: Boolean = false def successfulOpt: Some[this.type] = Some(this) def outputOpt: Some[os.Path] = Some(output) def dependencyClassPath: Seq[os.Path] = sources.resourceDirs ++ artifacts.classPath def dependencyCompileClassPath: Seq[os.Path] = sources.resourceDirs ++ artifacts.compileClassPath def fullClassPath: Seq[os.Path] = Seq(output) ++ dependencyClassPath def fullCompileClassPath: Seq[os.Path] = fullClassPath ++ dependencyCompileClassPath private lazy val mainClassesFoundInProject: Seq[String] = MainClass.find(output, logger).sorted private lazy val mainClassesFoundOnExtraClasspath: Seq[String] = options.classPathOptions.extraClassPath.flatMap(MainClass.find(_, logger)).sorted private lazy val mainClassesFoundInUserExtraDependencies: Seq[String] = artifacts.jarsForUserExtraDependencies.flatMap(MainClass.findInDependency).sorted def foundMainClasses(): Seq[String] = { val found = mainClassesFoundInProject ++ mainClassesFoundOnExtraClasspath if inputs.isEmpty && found.isEmpty then mainClassesFoundInUserExtraDependencies else found } def retainedMainClass( mainClasses: Seq[String], commandString: String, logger: Logger ): Either[BuildException, String] = { val defaultMainClassOpt = sources.defaultMainClass .filter(name => mainClasses.contains(name)) def foundMainClass: Either[BuildException, String] = mainClasses match { case Seq() => Left(new NoMainClassFoundError) case Seq(mainClass) => Right(mainClass) case _ => inferredMainClass(mainClasses, logger) .left.flatMap { mainClasses => // decode the names to present them to the user, // but keep the link to each original name to account for package prefixes: // "pack.Main$minus1" decodes to "pack.Main-1", which encodes back to "pack$u002EMain$minus1" // ^^^^^^^^^^^^^^^^----------------NOT THE SAME-----------------------^^^^^^^^^^^^^^^^^^^^^ val decodedToEncoded = mainClasses.map(mc => Name.decoded(mc) -> mc).toMap options.interactive.flatMap { interactive => interactive .chooseOne( "Found several main classes. Which would you like to run?", decodedToEncoded.keys.toList ) .map(decodedToEncoded(_)) // encode back the name of the chosen class .toRight { SeveralMainClassesFoundError( mainClasses = ::(mainClasses.head, mainClasses.tail.toList), commandString = commandString, positions = Nil ) } } } } defaultMainClassOpt match { case Some(cls) => Right(cls) case None => foundMainClass } } private def inferredMainClass( mainClasses: Seq[String], logger: Logger ): Either[Seq[String], String] = { val scriptInferredMainClasses = sources.inMemory.collect { case Sources.InMemory(_, _, _, Some(wrapperParams)) => wrapperParams.mainClass } .filter(mainClasses.contains(_)) val rawInputInferredMainClasses = mainClasses .filterNot(scriptInferredMainClasses.contains(_)) .filterNot(mainClassesFoundOnExtraClasspath.contains(_)) .filterNot(mainClassesFoundInUserExtraDependencies.contains(_)) val extraClasspathInferredMainClasses = mainClassesFoundOnExtraClasspath.filter(mainClasses.contains(_)) val userExtraDependenciesInferredMainClasses = mainClassesFoundInUserExtraDependencies.filter(mainClasses.contains(_)) def logMessageOnLesserPriorityMainClasses( pickedMainClass: String, mainClassDescriptor: String, lesserPriorityMainClasses: Seq[String] ): Unit = if lesserPriorityMainClasses.nonEmpty then { val first = lesserPriorityMainClasses.head val completeString = lesserPriorityMainClasses.mkString(", ") logger.message( s"""Running $pickedMainClass. Also detected $mainClassDescriptor: $completeString |You can run any one of them by passing option --main-class, i.e. --main-class $first |All available main classes can always be listed by passing option --list-main-classes""".stripMargin ) } ( rawInputInferredMainClasses, scriptInferredMainClasses, extraClasspathInferredMainClasses, userExtraDependenciesInferredMainClasses ) match { case (Seq(pickedMainClass), scriptInferredMainClasses, _, _) => logMessageOnLesserPriorityMainClasses( pickedMainClass = pickedMainClass, mainClassDescriptor = "script main classes", lesserPriorityMainClasses = scriptInferredMainClasses ) Right(pickedMainClass) case (rawMcs, scriptMcs, extraCpMcs, userExtraDepsMcs) if rawMcs.length > 1 => Left(rawMcs ++ scriptMcs ++ extraCpMcs ++ userExtraDepsMcs) case (Nil, Seq(pickedMainClass), _, _) => Right(pickedMainClass) case (Nil, scriptMcs, extraCpMcs, userExtraDepsMcs) if scriptMcs.length > 1 => Left(scriptMcs ++ extraCpMcs ++ userExtraDepsMcs) case (Nil, Nil, Seq(pickedMainClass), userExtraDepsMcs) => logMessageOnLesserPriorityMainClasses( pickedMainClass = pickedMainClass, mainClassDescriptor = "other main classes in dependencies", lesserPriorityMainClasses = userExtraDepsMcs ) Right(pickedMainClass) case (Nil, Nil, extraCpMcs, userExtraDepsMcs) if extraCpMcs.length > 1 => Left(extraCpMcs ++ userExtraDepsMcs) case (Nil, Nil, Nil, Seq(pickedMainClass)) => Right(pickedMainClass) case (Nil, Nil, Nil, userExtraDepsMcs) if userExtraDepsMcs.length > 1 => Left(userExtraDepsMcs) case (rawMcs, scriptMcs, extraCpMcs, userExtraDepsMcs) => Left(rawMcs ++ scriptMcs ++ extraCpMcs ++ userExtraDepsMcs) } } def retainedMainClassOpt( mainClasses: Seq[String], logger: Logger ): Option[String] = { val defaultMainClassOpt = sources.defaultMainClass .filter(name => mainClasses.contains(name)) def foundMainClass = mainClasses match { case Seq() => None case Seq(mainClass) => Some(mainClass) case _ => inferredMainClass(mainClasses, logger).toOption } defaultMainClassOpt.orElse(foundMainClass) } def crossKey: CrossKey = { val optKey = scalaParams.map { params => BuildOptions.CrossKey( params.scalaVersion, options.platform.value ) } CrossKey(optKey, scope) } } final case class Failed( inputs: Inputs, options: BuildOptions, scope: Scope, sources: Sources, artifacts: Artifacts, project: Project, diagnostics: Option[Seq[(Either[String, os.Path], bsp4j.Diagnostic)]] ) extends Build { def success: Boolean = false override def cancelled: Boolean = false def successfulOpt: None.type = None def outputOpt: None.type = None } final case class Cancelled( inputs: Inputs, options: BuildOptions, scope: Scope, reason: String ) extends Build { def success: Boolean = false def cancelled: Boolean = true def successfulOpt: None.type = None def outputOpt: None.type = None def diagnostics: None.type = None } /** If some options are manually overridden, append a hash of the options to the project name * Using only the command-line options not the ones from the sources. */ def updateInputs( inputs: Inputs, options: BuildOptions ): Inputs = { // If some options are manually overridden, append a hash of the options to the project name // Using options, not options0 - only the command-line options are taken into account. No hash is // appended for options from the sources. val optionsHash = options.hash inputs.copy(baseProjectName = inputs.baseProjectName + optionsHash.fold("")("_" + _)) } private def allInputs( inputs: Inputs, options: BuildOptions, logger: Logger )(using ScalaCliInvokeData) = CrossSources.forInputs( inputs, Sources.defaultPreprocessors( archiveCache = options.archiveCache, javaClassNameVersionOpt = options.internal.javaClassNameVersionOpt, javaCommand = () => options.javaHome().value.javaCommand ), logger, options.suppressWarningOptions, options.internal.exclude, download = options.downloader ) private def build( inputs: Inputs, crossSources: CrossSources, options: BuildOptions, logger: Logger, buildClient: BloopBuildClient, compiler: ScalaCompiler, docCompilerOpt: Option[ScalaCompiler], crossBuilds: Boolean, buildTests: Boolean, partial: Option[Boolean], actionableDiagnostics: Option[Boolean] )(using ScalaCliInvokeData): Either[BuildException, Builds] = either { val sharedOptions = crossSources.sharedOptions(options) val crossOptions = sharedOptions.crossOptions def doPostProcess(build: Build, inputs: Inputs, scope: Scope): Unit = build match { case build: Build.Successful => for (sv <- build.project.scalaCompiler.map(_.scalaVersion)) postProcess( generatedSources = build.generatedSources, generatedSrcRoot = inputs.generatedSrcRoot(scope), classesDir = build.output, logger = logger, workspace = inputs.workspace, updateSemanticDbs = true, scalaVersion = sv, buildOptions = build.options ).left.foreach(_.foreach(logger.message(_))) case _ => } final case class NonCrossBuilds( main: Build, testOpt: Option[Build], docOpt: Option[Build], testDocOpt: Option[Build] ) def doBuild(overrideOptions: BuildOptions): Either[BuildException, NonCrossBuilds] = either { val inputs0 = updateInputs( inputs, overrideOptions.orElse(options) // update hash in inputs with options coming from the CLI or cross-building, not from the sources ) val baseOptions = overrideOptions.orElse(sharedOptions) val scopedSources: ScopedSources = value(crossSources.scopedSources(baseOptions)) val mainSources: Sources = value(scopedSources.sources(Scope.Main, baseOptions, inputs.workspace, logger)) val mainOptions = mainSources.buildOptions val testSources: Sources = value(scopedSources.sources(Scope.Test, baseOptions, inputs.workspace, logger)) val testOptions = testSources.buildOptions def doBuildScope( options: BuildOptions, sources: Sources, scope: Scope, actualCompiler: ScalaCompiler = compiler ): Either[BuildException, Build] = either { val sources0 = sources.withVirtualDir(inputs0, scope, options) val generatedSources = sources0.generateSources(inputs0.generatedSrcRoot(scope)) val res = build( inputs = inputs0, sources = sources0, generatedSources = generatedSources, options = options, scope = scope, logger = logger, buildClient = buildClient, compiler = actualCompiler, buildTests = buildTests, partial = partial, actionableDiagnostics = actionableDiagnostics ) value(res) } val mainBuild = value(doBuildScope(mainOptions, mainSources, Scope.Main)) val mainDocBuildOpt = docCompilerOpt match { case None => None case Some(docCompiler) => Some(value(doBuildScope( options = mainOptions, sources = mainSources, scope = Scope.Main, actualCompiler = docCompiler ))) } def testBuildOpt(doc: Boolean = false): Either[BuildException, Option[Build]] = either { if buildTests then { val actualCompilerOpt = if doc then docCompilerOpt else Some(compiler) actualCompilerOpt match { case None => None case Some(actualCompiler) => val testBuild = value { mainBuild match { case s: Build.Successful => val extraTestOptions = BuildOptions( classPathOptions = ClassPathOptions( extraClassPath = Seq(s.output) ) ) val testOptions0 = { val testOrExtra = extraTestOptions.orElse(testOptions) testOrExtra .copy(scalaOptions = // Scala options between scopes need to be compatible mainOptions.scalaOptions.orElse(testOrExtra.scalaOptions) ) } val isScala2 = value(testOptions0.scalaParams).exists(_.scalaVersion.startsWith("2.")) val finalSources = if doc && isScala2 then testSources.withExtraSources(mainSources) else testSources doBuildScope( options = testOptions0, sources = finalSources, scope = Scope.Test, actualCompiler = actualCompiler ) case _ => Right(Build.Cancelled( inputs, sharedOptions, Scope.Test, "Parent build failed or cancelled" )) } } Some(testBuild) } } else None } val testBuildOpt0 = value(testBuildOpt()) doPostProcess(mainBuild, inputs0, Scope.Main) for (testBuild <- testBuildOpt0) doPostProcess(testBuild, inputs0, Scope.Test) val docTestBuildOpt0 = value(testBuildOpt(doc = true)) NonCrossBuilds(mainBuild, testBuildOpt0, mainDocBuildOpt, docTestBuildOpt0) } def buildScopes(): Either[BuildException, Builds] = either { val nonCrossBuilds = value(doBuild(BuildOptions())) val (extraMainBuilds, extraTestBuilds, extraDocBuilds, extraDocTestBuilds) = if crossBuilds then { val extraBuilds = value { val maybeBuilds = crossOptions.map(doBuild) maybeBuilds .sequence .left.map(CompositeBuildException(_)) } ( extraBuilds.map(_.main), extraBuilds.flatMap(_.testOpt), extraBuilds.flatMap(_.docOpt), extraBuilds.flatMap(_.testDocOpt) ) } else { if crossOptions.nonEmpty then { val crossBuildParams: Seq[CrossBuildParams] = crossOptions.map(CrossBuildParams(_)) logger.message( s"""Cross-building is disabled, ignoring ${crossOptions.length} builds: | ${crossBuildParams.map(_.asString).mkString("\n ")} |Cross builds are only available when the --cross option is passed. |Defaulting to ${CrossBuildParams(options).asString}""".stripMargin ) } (Nil, Nil, Nil, Nil) } Builds( builds = Seq(nonCrossBuilds.main) ++ nonCrossBuilds.testOpt.toSeq, crossBuilds = Seq(extraMainBuilds, extraTestBuilds), docBuilds = nonCrossBuilds.docOpt.toSeq ++ nonCrossBuilds.testDocOpt.toSeq, docCrossBuilds = Seq(extraDocBuilds, extraDocTestBuilds) ) } val builds = value(buildScopes()) ResourceMapper.copyResourceToClassesDir(builds.main) for (testBuild <- builds.get(Scope.Test)) ResourceMapper.copyResourceToClassesDir(testBuild) if actionableDiagnostics.getOrElse(true) then { val projectOptions = builds.get(Scope.Test).getOrElse(builds.main).options projectOptions.logActionableDiagnostics(logger) } builds } private def build( inputs: Inputs, sources: Sources, generatedSources: Seq[GeneratedSource], options: BuildOptions, scope: Scope, logger: Logger, buildClient: BloopBuildClient, compiler: ScalaCompiler, buildTests: Boolean, partial: Option[Boolean], actionableDiagnostics: Option[Boolean] )(using ScalaCliInvokeData): Either[BuildException, Build] = either { val build0 = value { buildOnce( inputs = inputs, sources = sources, generatedSources = generatedSources, options = options, scope = scope, logger = logger, buildClient = buildClient, compiler = compiler, partialOpt = partial ) } build0 match { case successful: Successful => if options.jmhOptions.canRunJmh && scope == Scope.Main then value { val res = jmhBuild( inputs = inputs, build = successful, logger = logger, successful.options.javaHome().value.javaCommand, buildClient = buildClient, compiler = compiler, buildTests = buildTests, actionableDiagnostics = actionableDiagnostics ) res.flatMap { case Some(b) => Right(b) case None => Left(new JmhBuildFailedError) } } else build0 case _ => build0 } } def projectRootDir(root: os.Path, projectName: String): os.Path = root / Constants.workspaceDirName / projectName def classesRootDir(root: os.Path, projectName: String): os.Path = projectRootDir(root, projectName) / "classes" def classesDir(root: os.Path, projectName: String, scope: Scope, suffix: String = ""): os.Path = classesRootDir(root, projectName) / s"${scope.name}$suffix" def resourcesRegistry( root: os.Path, projectName: String, scope: Scope ): os.Path = root / Constants.workspaceDirName / projectName / s"resources-${scope.name}" def scalaNativeSupported( options: BuildOptions, inputs: Inputs, logger: Logger ): Either[BuildException, Option[ScalaNativeCompatibilityError]] = either { val scalaParamsOpt = value(options.scalaParams) scalaParamsOpt.flatMap { scalaParams => val scalaVersion = scalaParams.scalaVersion val nativeVersionMaybe = options.scalaNativeOptions.numeralVersion def snCompatError = Left( new ScalaNativeCompatibilityError( scalaVersion, options.scalaNativeOptions.finalVersion ) ) def warnIncompatibleNativeOptions(numeralVersion: SNNumeralVersion) = if numeralVersion < SNNumeralVersion(0, 4, 4) && options.scalaNativeOptions.embedResources.isDefined then logger.diagnostic( "This Scala Version cannot embed resources, regardless of the options used." ) val numeralOrError: Either[ScalaNativeCompatibilityError, SNNumeralVersion] = nativeVersionMaybe match { case Some(snNumeralVer) => if snNumeralVer < SNNumeralVersion(0, 4, 1) && Properties.isWin then snCompatError else if scalaVersion.startsWith("3.0") then snCompatError else if scalaVersion.startsWith("3") then if snNumeralVer >= SNNumeralVersion(0, 4, 3) then Right(snNumeralVer) else snCompatError else if scalaVersion.startsWith("2.13") then Right(snNumeralVer) else if scalaVersion.startsWith("2.12") then if inputs.sourceFiles().forall { case _: AnyScript => snNumeralVer >= SNNumeralVersion(0, 4, 3) case _ => true } then Right(snNumeralVer) else snCompatError else snCompatError case None => snCompatError } numeralOrError match { case Left(compatError) => Some(compatError) case Right(snNumeralVersion) => warnIncompatibleNativeOptions(snNumeralVersion) None } } } def build( inputs: Inputs, options: BuildOptions, compilerMaker: ScalaCompilerMaker, docCompilerMakerOpt: Option[ScalaCompilerMaker], logger: Logger, crossBuilds: Boolean, buildTests: Boolean, partial: Option[Boolean], actionableDiagnostics: Option[Boolean] )(using ScalaCliInvokeData): Either[BuildException, Builds] = either { val buildClient = BloopBuildClient.create( logger = logger, keepDiagnostics = options.internal.keepDiagnostics ) val classesDir0 = classesRootDir(inputs.workspace, inputs.projectName) val (crossSources: CrossSources, inputs0) = value(allInputs(inputs, options, logger)) val buildOptions = crossSources.sharedOptions(options) if !buildOptions.suppressWarningOptions.suppressDeprecatedFeatureWarning.getOrElse(false) && buildOptions.scalaParams.exists(_.exists(_.scalaVersion == "2.12.4") && !buildOptions.useBuildServer.contains(false)) then logger.message( s"""[${Console.YELLOW}warn${Console.RESET}] Scala 2.12.4 has been deprecated for use with Bloop. |[${Console.YELLOW}warn${Console.RESET}] It may lead to infinite compilation. |[${Console.YELLOW}warn${Console.RESET}] To disable the build server, pass ${Console.BOLD}--server=false${Console.RESET}. |[${Console.YELLOW}warn${Console.RESET}] Refer to https://github.com/VirtusLab/scala-cli/issues/1382 and https://github.com/sbt/zinc/issues/1010""".stripMargin ) value { compilerMaker.withCompiler( workspace = inputs0.workspace / Constants.workspaceDirName, classesDir = classesDir0, buildClient = buildClient, logger = logger, buildOptions = buildOptions ) { compiler => docCompilerMakerOpt match { case None => logger.debug("No doc compiler provided, skipping") build( inputs = inputs0, crossSources = crossSources, options = options, logger = logger, buildClient = buildClient, compiler = compiler, docCompilerOpt = None, crossBuilds = crossBuilds, buildTests = buildTests, partial = partial, actionableDiagnostics = actionableDiagnostics ) case Some(docCompilerMaker) => docCompilerMaker.withCompiler( workspace = inputs0.workspace / Constants.workspaceDirName, classesDir = classesDir0, // ??? buildClient = buildClient, logger = logger, buildOptions = buildOptions ) { docCompiler => build( inputs = inputs0, crossSources = crossSources, options = options, logger = logger, buildClient = buildClient, compiler = compiler, docCompilerOpt = Some(docCompiler), crossBuilds = crossBuilds, buildTests = buildTests, partial = partial, actionableDiagnostics = actionableDiagnostics ) } } } } } def validate( logger: Logger, options: BuildOptions ): Either[BuildException, Unit] = { val (errors, otherDiagnostics) = options.validate.partition(_.severity == Severity.Error) logger.log(otherDiagnostics) if errors.nonEmpty then Left(CompositeBuildException(errors.map(new ValidationException(_)))) else Right(()) } def watch( inputs: Inputs, options: BuildOptions, compilerMaker: ScalaCompilerMaker, docCompilerMakerOpt: Option[ScalaCompilerMaker], logger: Logger, crossBuilds: Boolean, buildTests: Boolean, partial: Option[Boolean], actionableDiagnostics: Option[Boolean], postAction: () => Unit = () => () )(action: Either[BuildException, Builds] => Unit)(using ScalaCliInvokeData): Watcher = { val buildClient = BloopBuildClient.create( logger, keepDiagnostics = options.internal.keepDiagnostics ) val threads = BuildThreads.create() val classesDir0 = classesRootDir(inputs.workspace, inputs.projectName) lazy val compilers: Either[BuildException, (ScalaCompiler, Option[ScalaCompiler])] = either { val (crossSources: CrossSources, inputs0: Inputs) = value(allInputs(inputs, options, logger)) val sharedOptions = crossSources.sharedOptions(options) val compiler = value { compilerMaker.create( workspace = inputs0.workspace / Constants.workspaceDirName, classesDir = classesDir0, buildClient = buildClient, logger = logger, buildOptions = sharedOptions ) } val docCompilerOpt = docCompilerMakerOpt.map(_.create( workspace = inputs0.workspace / Constants.workspaceDirName, classesDir = classesDir0, buildClient = buildClient, logger = logger, buildOptions = sharedOptions )).map(value) compiler -> docCompilerOpt } def info: Either[BuildException, (ScalaCompiler, Option[ScalaCompiler], CrossSources, Inputs)] = either { val (crossSources: CrossSources, inputs0: Inputs) = value(allInputs(inputs, options, logger)) val (compiler, docCompilerOpt) = value(compilers) (compiler, docCompilerOpt, crossSources, inputs0) } var res: Either[BuildException, Builds] = null def run(): Unit = { try { res = info.flatMap { case ( compiler: ScalaCompiler, docCompilerOpt: Option[ScalaCompiler], crossSources: CrossSources, inputs: Inputs ) => build( inputs = inputs, crossSources = crossSources, options = options, logger = logger, buildClient = buildClient, compiler = compiler, docCompilerOpt = docCompilerOpt, crossBuilds = crossBuilds, buildTests = buildTests, partial = partial, actionableDiagnostics = actionableDiagnostics ) } action(res) } catch { case NonFatal(e) => Util.printException(e) } postAction() } run() val watcher = new Watcher(ListBuffer(), threads.fileWatcher, run(), info.foreach(_._1.shutdown())) def doWatch(): Unit = either { val (crossSources: CrossSources, inputs0: Inputs) = value(allInputs(inputs, options, logger)) val mergedOptions = crossSources.sharedOptions(options) val elements: Seq[Element] = if res == null then inputs0.elements else res .map { builds => val allResourceDirectories = crossSources.resourceDirs.map(rd => ResourceDirectory(rd.value)) val mainElems = builds.main.inputs.elements val testElems = builds.get(Scope.Test).map(_.inputs.elements).getOrElse(Nil) (mainElems ++ testElems ++ allResourceDirectories).distinct } .getOrElse(inputs.elements) for (elem <- elements) { val depth = elem match { case _: SingleFile => -1 case _ => Int.MaxValue } val eventFilter: PathWatchers.Event => Boolean = elem match { case d: Directory => // Filtering event for directories, to ignore those related to the .bloop directory in particular event => val p = os.Path(event.getTypedPath.getPath.toAbsolutePath) val relPath = p.relativeTo(d.path) val isHidden = relPath.segments.exists(_.startsWith(".")) val pathLast = relPath.lastOpt.orElse(p.lastOpt).getOrElse("") def isScalaFile = pathLast.endsWith(".sc") || pathLast.endsWith(".scala") def isJavaFile = pathLast.endsWith(".java") !isHidden && (isScalaFile || isJavaFile) case _ => _ => true } val watcher0 = watcher.newWatcher() elem match { case d: OnDisk => watcher0.register(d.path.toNIO, depth) case _: Virtual => } watcher0.addObserver { onChangeBufferedObserver(event => if eventFilter(event) then watcher.schedule()) } } val artifacts = res .map { builds => def artifacts(build: Build): Seq[os.Path] = build.successfulOpt.toSeq.flatMap(_.artifacts.classPath) val main = artifacts(builds.main) val test = builds.get(Scope.Test).map(artifacts).getOrElse(Nil) val allScopesArtifacts = (main ++ test).distinct allScopesArtifacts .filterNot(_.segments.contains(Constants.workspaceDirName)) } .getOrElse(Nil) for (artifact <- artifacts) { val depth = if os.isFile(artifact) then -1 else Int.MaxValue val watcher0 = watcher.newWatcher() watcher0.register(artifact.toNIO, depth) watcher0.addObserver(onChangeBufferedObserver(_ => watcher.schedule())) } val extraWatchPaths = mergedOptions.watchOptions.extraWatchPaths.distinct for (extraPath <- extraWatchPaths) if os.exists(extraPath) then { val depth = if os.isFile(extraPath) then -1 else Int.MaxValue val watcher0 = watcher.newWatcher() watcher0.register(extraPath.toNIO, depth) watcher0.addObserver(onChangeBufferedObserver(_ => watcher.schedule())) } else logger.message(s"$warnPrefix provided watched path doesn't exist: $extraPath") } try doWatch() catch { case NonFatal(e) => watcher.dispose() throw e } watcher } def releaseFlag( options: BuildOptions, compilerJvmVersionOpt: Option[Positioned[Int]], logger: Logger ): Option[Int] = { lazy val javaHome = options.javaHome() if compilerJvmVersionOpt.exists(javaHome.value.version > _.value) then { logger.log(List(Diagnostic( Diagnostic.Messages.bloopTooOld, Severity.Warning, javaHome.positions ++ compilerJvmVersionOpt.map(_.positions).getOrElse(Nil) ))) None } else if compilerJvmVersionOpt.exists(_.value == 8) then None else if options.scalaOptions.scalacOptions.values.exists(opt => opt.headOption.exists(_.value.value.startsWith("-release")) || opt.headOption.exists(_.value.value.startsWith("-java-output-version")) ) then None else if compilerJvmVersionOpt.isEmpty && javaHome.value.version == 8 then None else Some(javaHome.value.version) } /** Builds a Bloop project. * * @param inputs * inputs to be included in the project * @param sources * sources to be included in the project * @param generatedSources * sources generated by Scala CLI as part of the build * @param options * build options * @param compilerJvmVersionOpt * compiler JVM version (optional) * @param scope * build scope for which the project is to be created * @param logger * logger * @param maybeRecoverOnError * a function handling [[BuildException]] instances, possibly recovering them; returns None on * recovery, Some(e: BuildException) otherwise * @return * a bloop [[Project]] */ def buildProject( inputs: Inputs, sources: Sources, generatedSources: Seq[GeneratedSource], options: BuildOptions, scope: Scope, logger: Logger, artifacts: Artifacts, maybeRecoverOnError: BuildException => Option[BuildException] = e => Some(e) ): Either[BuildException, Project] = either { val allSources = sources.paths.map(_._1) ++ generatedSources.map(_.generated) val classesDir0 = classesDir(inputs.workspace, inputs.projectName, scope) val scaladocDir = classesDir(inputs.workspace, inputs.projectName, scope, suffix = "-doc") val generateSemanticDbs = options.scalaOptions.semanticDbOptions.generateSemanticDbs.getOrElse(false) val semanticDbTargetRoot = options.scalaOptions.semanticDbOptions.semanticDbTargetRoot val semanticDbSourceRoot = options.scalaOptions.semanticDbOptions.semanticDbSourceRoot.getOrElse(inputs.workspace) val scalaCompilerParamsOpt = artifacts.scalaOpt match { case Some(scalaArtifacts) => val params = value { options.scalaParams match { case Left(buildException) if maybeRecoverOnError(buildException).isEmpty => Right(None) // this will effectively try to fall back to a pure Java build case otherwise => otherwise } }.getOrElse { sys.error( "Should not happen (inconsistency between Scala parameters in BuildOptions and ScalaArtifacts)" ) } val pluginScalacOptions = scalaArtifacts.compilerPlugins.map { case (_, _, path) => ScalacOpt(s"-Xplugin:$path") }.distinct val semanticDbTargetRootOptions: Seq[ScalacOpt] = (semanticDbTargetRoot match case Some(targetRoot) if params.scalaVersion.startsWith("2.") => Seq(s"-P:semanticdb:targetroot:$targetRoot") case Some(targetRoot) => Seq("-semanticdb-target", targetRoot.toString) case None => Nil ).map(ScalacOpt(_)) val semanticDbScalacOptions: Seq[ScalacOpt] = if generateSemanticDbs then semanticDbTargetRootOptions ++ ( if params.scalaVersion.startsWith("2.") then Seq( "-Yrangepos", "-P:semanticdb:failures:warning", "-P:semanticdb:synthetics:on", s"-P:semanticdb:sourceroot:$semanticDbSourceRoot" ) else Seq("-Xsemanticdb", "-sourceroot", semanticDbSourceRoot.toString) ).map(ScalacOpt(_)) else Nil val sourceRootScalacOptions = if params.scalaVersion.startsWith("2.") then Nil else Seq("-sourceroot", inputs.workspace.toString).map(ScalacOpt(_)) val scalaJsScalacOptions = if options.platform.value == Platform.JS && !params.scalaVersion.startsWith("2.") then Seq(ScalacOpt("-scalajs")) else Nil val scalapyOptions = if params.scalaVersion.startsWith("2.13.") && options.notForBloopOptions.python.getOrElse(false) then Seq(ScalacOpt("-Yimports:java.lang,scala,scala.Predef,me.shadaj.scalapy")) else Nil val scalacOptions = options.scalaOptions.scalacOptions.map(_.value) ++ pluginScalacOptions ++ semanticDbScalacOptions ++ sourceRootScalacOptions ++ scalaJsScalacOptions ++ scalapyOptions val compilerParams = ScalaCompilerParams( scalaVersion = params.scalaVersion, scalaBinaryVersion = params.scalaBinaryVersion, scalacOptions = scalacOptions.toSeq.map(_.value), compilerClassPath = scalaArtifacts.compilerClassPath, bridgeJarsOpt = scalaArtifacts.bridgeJarsOpt.map(_.headOption.toSeq) ) Some(compilerParams) case None => None } val javacOptions = { val semanticDbJavacOptions = // FIXME Should this be in scalaOptions, now that we use it for javac stuff too? if generateSemanticDbs then { // from https://github.com/scalameta/metals/blob/04405c0401121b372ea1971c361e05108fb36193/metals/src/main/scala/scala/meta/internal/metals/JavaInteractiveSemanticdb.scala#L137-L146 val compilerPackages = Seq( "com.sun.tools.javac.api", "com.sun.tools.javac.code", "com.sun.tools.javac.model", "com.sun.tools.javac.tree", "com.sun.tools.javac.util" ) val exports = compilerPackages.flatMap { pkg => Seq("-J--add-exports", s"-Jjdk.compiler/$pkg=ALL-UNNAMED") } val javacTargetRoot = semanticDbTargetRoot.getOrElse("javac-classes-directory") Seq( // does the path need to be escaped somehow? s"-Xplugin:semanticdb -sourceroot:$semanticDbSourceRoot -targetroot:$javacTargetRoot" ) ++ exports } else Nil semanticDbJavacOptions ++ options.javaOptions.javacOptions.map(_.value) } // `test` scope should contains class path to main scope val mainClassesPath = if scope == Scope.Test then List(classesDir(inputs.workspace, inputs.projectName, Scope.Main)) else Nil value(validate(logger, options)) val fullClassPath = artifacts.compileClassPath ++ mainClassesPath ++ artifacts.javacPluginDependencies.map(_._3) ++ artifacts.extraJavacPlugins val project = Project( directory = inputs.workspace / Constants.workspaceDirName, argsFilePath = projectRootDir(inputs.workspace, inputs.projectName) / Constants.scalacArgumentsFileName, workspace = inputs.workspace, classesDir = classesDir0, scaladocDir = scaladocDir, scalaCompiler = scalaCompilerParamsOpt, scalaJsOptions = if options.platform.value == Platform.JS then Some(value(options.scalaJsOptions.config(logger, maybeRecoverOnError))) else None, scalaNativeOptions = if options.platform.value == Platform.Native then Some(options.scalaNativeOptions.bloopConfig()) else None, projectName = inputs.scopeProjectName(scope), classPath = fullClassPath, resolution = Some(Project.resolution(artifacts.detailedArtifacts)), sources = allSources, resourceDirs = sources.resourceDirs, scope = scope, javaHomeOpt = Option(options.javaHomeLocation().value), javacOptions = javacOptions.toList ) project } def prepareBuild( inputs: Inputs, sources: Sources, generatedSources: Seq[GeneratedSource], options: BuildOptions, scope: Scope, compiler: ScalaCompiler, logger: Logger, buildClient: BloopBuildClient, maybeRecoverOnError: BuildException => Option[BuildException] = e => Some(e) ): Either[BuildException, (os.Path, Option[ScalaParameters], Artifacts, Project, Boolean)] = either { val options0 = if sources.hasJava && !sources.hasScala then options.copy( scalaOptions = options.scalaOptions.copy( scalaVersion = options.scalaOptions.scalaVersion.orElse { Some(MaybeScalaVersion.none) } ) ) else options val params = value(options0.scalaParams) val scopeParams = if scope == Scope.Main then Nil else Seq(scope.name) buildClient.setProjectParams(scopeParams ++ value(options0.projectParams)) val classesDir0 = classesDir(inputs.workspace, inputs.projectName, scope) val artifacts = value(options0.artifacts(logger, scope, maybeRecoverOnError)) value(validate(logger, options0)) val project = value { buildProject( inputs = inputs, sources = sources, generatedSources = generatedSources, options = options0, scope = scope, logger = logger, artifacts = artifacts, maybeRecoverOnError = maybeRecoverOnError ) } val projectChanged = compiler.prepareProject(project, logger) if projectChanged then { if compiler.usesClassDir && os.isDir(classesDir0) then { logger.debug(s"Clearing $classesDir0") os.list(classesDir0).foreach { p => logger.debug(s"Removing $p") try os.remove.all(p) catch { case ex: FileSystemException => logger.debug(s"Ignoring $ex while cleaning up $p") } } } if os.exists(project.argsFilePath) then { logger.debug(s"Removing ${project.argsFilePath}") try os.remove(project.argsFilePath) catch { case ex: FileSystemException => logger.debug(s"Ignoring $ex while cleaning up ${project.argsFilePath}") } } } (classesDir0, params, artifacts, project, projectChanged) } def buildOnce( inputs: Inputs, sources: Sources, generatedSources: Seq[GeneratedSource], options: BuildOptions, scope: Scope, logger: Logger, buildClient: BloopBuildClient, compiler: ScalaCompiler, partialOpt: Option[Boolean] ): Either[BuildException, Build] = either { if options.platform.value == Platform.Native then value(scalaNativeSupported(options, inputs, logger)) match { case None => case Some(error) => value(Left(error)) } val (classesDir0, scalaParams, artifacts, project, _) = value { prepareBuild( inputs = inputs, sources = sources, generatedSources = generatedSources, options = options, scope = scope, compiler = compiler, logger = logger, buildClient = buildClient ) } if sources.hasJava && sources.hasScala && options.useBuildServer.contains(false) then { val javaPaths = sources.paths .filter(_._1.last.endsWith(".java")) .map(_._1.toString) ++ sources.inMemory .filter(_.generatedRelPath.last.endsWith(".java")) .map(_.originalPath.fold(identity, _._2.toString)) val javaPathsList = javaPaths.map(p => s" $p").mkString(System.lineSeparator()) logger.message( s"""$warnPrefix With ${Console.BOLD}--server=false${Console.RESET}, .java files are not compiled to .class files. |scalac parses .java sources for type information (cross-compilation), but without the build server (Bloop/Zinc) nothing compiles them to bytecode. |Affected .java files: |$javaPathsList |Remove --server=false or compile Java files separately to avoid runtime NoClassDefFoundError.""".stripMargin ) } buildClient.clear() buildClient.setGeneratedSources(scope, generatedSources) val partial = partialOpt.getOrElse { options.notForBloopOptions.packageOptions.packageTypeOpt.exists(_.sourceBased) } val success = partial || compiler.compile(project, logger) if success then Successful( inputs = inputs, options = options, scalaParams, scope = scope, sources = sources, artifacts = artifacts, project = project, output = classesDir0, diagnostics = buildClient.diagnostics, generatedSources = generatedSources, isPartial = partial, logger = logger ) else Failed( inputs = inputs, options = options, scope = scope, sources = sources, artifacts = artifacts, project = project, diagnostics = buildClient.diagnostics ) } def postProcess( generatedSources: Seq[GeneratedSource], generatedSrcRoot: os.Path, classesDir: os.Path, logger: Logger, workspace: os.Path, updateSemanticDbs: Boolean, scalaVersion: String, buildOptions: BuildOptions ): Either[Seq[String], Unit] = if os.exists(classesDir) then { // TODO Write classes to a separate directory during post-processing logger.debug("Post-processing class files of pre-processed sources") val mappings = generatedSources .map { source => val relPath = source.generated.relativeTo(generatedSrcRoot).toString val reportingPath = source.reportingPath.fold(s => s, _.last) (relPath, (reportingPath, scalaLineToScLineShift(source.wrapperParamsOpt))) } .toMap val postProcessors = Seq(ByteCodePostProcessor) ++ (if updateSemanticDbs then Seq(SemanticDbPostProcessor) else Nil) ++ Seq(TastyPostProcessor) val failures = postProcessors.flatMap( _.postProcess( generatedSources = generatedSources, mappings = mappings, workspace = workspace, output = classesDir, logger = logger, scalaVersion = scalaVersion, buildOptions = buildOptions ) .fold(e => Seq(e), _ => Nil) ) if failures.isEmpty then Right(()) else Left(failures) } else Right(()) def onChangeBufferedObserver(onEvent: PathWatchers.Event => Unit): Observer[PathWatchers.Event] = new Observer[PathWatchers.Event] { def onError(t: Throwable): Unit = { // TODO Log that properly System.err.println("got error:") @tailrec def printEx(t: Throwable): Unit = if t != null then { System.err.println(t) System.err.println( t.getStackTrace.iterator.map(" " + _ + System.lineSeparator()).mkString ) printEx(t.getCause) } printEx(t) } def onNext(event: PathWatchers.Event): Unit = onEvent(event) } final class Watcher( val watchers: ListBuffer[PathWatcher[PathWatchers.Event]], val scheduler: ScheduledExecutorService, onChange: => Unit, onDispose: => Unit ) { def newWatcher(): PathWatcher[PathWatchers.Event] = { val w = PathWatchers.get(true) watchers += w w } def dispose(): Unit = { onDispose watchers.foreach(_.close()) scheduler.shutdown() } private val lock = new Object private var f: ScheduledFuture[?] = uninitialized private val waitFor = 50.millis private val runnable: Runnable = { () => lock.synchronized { f = null } onChange // FIXME Log exceptions } def schedule(): Unit = if f == null then lock.synchronized { if f == null then f = scheduler.schedule(runnable, waitFor.length, waitFor.unit) } } private def printable(path: os.Path): String = if path.startsWith(os.pwd) then path.relativeTo(os.pwd).toString else path.toString private def jmhBuild( inputs: Inputs, build: Build.Successful, logger: Logger, javaCommand: String, buildClient: BloopBuildClient, compiler: ScalaCompiler, buildTests: Boolean, actionableDiagnostics: Option[Boolean] )(using ScalaCliInvokeData): Either[BuildException, Option[Build]] = either { val jmhProjectName = inputs.projectName + "_jmh" val jmhOutputDir = inputs.workspace / Constants.workspaceDirName / jmhProjectName os.remove.all(jmhOutputDir) val jmhSourceDir = jmhOutputDir / "sources" val jmhResourceDir = jmhOutputDir / "resources" val retCode = run( javaCommand, build.fullClassPath.map(_.toIO), "org.openjdk.jmh.generators.bytecode.JmhBytecodeGenerator", Seq(printable(build.output), printable(jmhSourceDir), printable(jmhResourceDir), "default"), logger ) if retCode != 0 then { val red = Console.RED val lightRed = "\u001b[91m" val reset = Console.RESET System.err.println( s"${red}jmh bytecode generator exited with return code $lightRed$retCode$red.$reset" ) } if retCode == 0 then { val jmhInputs = inputs.copy( baseProjectName = jmhProjectName, // hash of the underlying project if needed is already in jmhProjectName mayAppendHash = false, elements = inputs.elements ++ Seq( Directory(jmhSourceDir), ResourceDirectory(jmhResourceDir) ) ) val updatedOptions = build.options.copy( jmhOptions = build.options.jmhOptions.copy( runJmh = build.options.jmhOptions.runJmh.map(_ => false) ) ) val (crossSources, inputs0) = value(allInputs(jmhInputs, updatedOptions, logger)) val jmhBuilds = value { Build.build( inputs0, crossSources, updatedOptions, logger, buildClient, compiler, None, crossBuilds = false, buildTests = buildTests, partial = None, actionableDiagnostics = actionableDiagnostics ) } Some(jmhBuilds.main) } else None } private def run( javaCommand: String, classPath: Seq[File], mainClass: String, args: Seq[String], logger: Logger ): Int = { val command = Seq(javaCommand) ++ Seq( "-cp", classPath.iterator.map(_.getAbsolutePath).mkString(File.pathSeparator), mainClass ) ++ args logger.log( s"Running ${command.mkString(" ")}", " Running" + System.lineSeparator() + command.iterator.map(_ + System.lineSeparator()).mkString ) new ProcessBuilder(command*) .inheritIO() .start() .waitFor() } } ================================================ FILE: modules/build/src/main/scala/scala/build/BuildThreads.scala ================================================ package scala.build import java.util.concurrent.{Executors, ScheduledExecutorService} import scala.build.internal.Util final case class BuildThreads( bloop: _root_.bloop.rifle.BloopThreads, fileWatcher: ScheduledExecutorService ) { def shutdown(): Unit = { bloop.shutdown() fileWatcher.shutdown() } } object BuildThreads { def create(): BuildThreads = { val bloop = _root_.bloop.rifle.BloopThreads.create() val fileWatcher = Executors.newSingleThreadScheduledExecutor( Util.daemonThreadFactory("scala-cli-file-watcher") ) BuildThreads(bloop, fileWatcher) } } ================================================ FILE: modules/build/src/main/scala/scala/build/Builds.scala ================================================ package scala.build import scala.build.options.Scope final case class Builds( builds: Seq[Build], crossBuilds: Seq[Seq[Build]], docBuilds: Seq[Build], docCrossBuilds: Seq[Seq[Build]] ) { def main: Build = get(Scope.Main).getOrElse { sys.error("No main build found") } def get(scope: Scope): Option[Build] = builds.find(_.scope == scope) def anyFailed: Boolean = !all.forall(_.success) def all: Seq[Build] = builds ++ crossBuilds.flatten lazy val map: Map[CrossKey, Build.Successful] = all .collect { case s: Build.Successful => s } .map(b => b.crossKey -> b) .toMap def allDoc: Seq[Build] = docBuilds ++ docCrossBuilds.flatten } ================================================ FILE: modules/build/src/main/scala/scala/build/CollectionOps.scala ================================================ package scala.build import scala.collection.mutable object CollectionOps { extension [T](items: Seq[T]) { /** Works the same standard lib's `distinct`, but only differentiates based on the key extracted * by the passed function. If more than one value exists for the same key, only the first one * is kept, the rest is filtered out. * * @param f * function to extract the key used for distinction * @tparam K * type of the key used for distinction * @return * the sequence of items with distinct [[items]].map(f) */ def distinctBy[K](f: T => K): Seq[T] = if items.length == 1 then items else val seen = mutable.HashSet.empty[K] items.filter { item => val key = f(item) if seen(key) then false else seen += key true } } } ================================================ FILE: modules/build/src/main/scala/scala/build/ConsoleBloopBuildClient.scala ================================================ package scala.build import ch.epfl.scala.bsp4j import java.io.File import java.net.URI import java.nio.file.{NoSuchFileException, Paths} import scala.build.errors.Severity import scala.build.internal.WrapperParams import scala.build.internals.ConsoleUtils.ScalaCliConsole import scala.build.options.Scope import scala.build.postprocessing.LineConversion.scalaLineToScLine import scala.collection.mutable import scala.jdk.CollectionConverters.* class ConsoleBloopBuildClient( logger: Logger, keepDiagnostics: Boolean = false, generatedSources: mutable.Map[Scope, Seq[GeneratedSource]] = mutable.Map() ) extends BloopBuildClient { import ConsoleBloopBuildClient._ private var projectParams = Seq.empty[String] private def projectNameSuffix = if (projectParams.isEmpty) "" else " (" + projectParams.mkString(", ") + ")" private def projectName = "project" + projectNameSuffix private var printedStart = false private val diagnostics0 = new mutable.ListBuffer[(Either[String, os.Path], bsp4j.Diagnostic)] def setGeneratedSources(scope: Scope, newGeneratedSources: Seq[GeneratedSource]) = generatedSources(scope) = newGeneratedSources def setProjectParams(newParams: Seq[String]): Unit = { projectParams = newParams } def diagnostics: Option[Seq[(Either[String, os.Path], bsp4j.Diagnostic)]] = if (keepDiagnostics) Some(diagnostics0.result()) else None private def postProcessDiagnostic( path: os.Path, diag: bsp4j.Diagnostic, diagnosticMappings: Map[os.Path, (Either[String, os.Path], Option[WrapperParams])] ): Option[(Either[String, os.Path], bsp4j.Diagnostic)] = diagnosticMappings.get(path).map { case (originalPath, wrapperParamsOpt) => ( originalPath, scalaLineToScLine(diag.getRange.getStart.getLine, wrapperParamsOpt), scalaLineToScLine(diag.getRange.getStart.getLine, wrapperParamsOpt) ) }.collect { case (originalPath, Some(scLineStart), Some(scLineEnd)) => val start = new bsp4j.Position(scLineStart, diag.getRange.getStart.getCharacter) val end = new bsp4j.Position(scLineEnd, diag.getRange.getEnd.getCharacter) val range = new bsp4j.Range(start, end) val updatedDiag = new bsp4j.Diagnostic(range, diag.getMessage) updatedDiag.setCode(diag.getCode) updatedDiag.setRelatedInformation(diag.getRelatedInformation) updatedDiag.setSeverity(diag.getSeverity) updatedDiag.setSource(diag.getSource) updatedDiag.setData(diag.getData) (originalPath, updatedDiag) } override def onBuildPublishDiagnostics(params: bsp4j.PublishDiagnosticsParams): Unit = { logger.debug("Received onBuildPublishDiagnostics from bloop: " + params) for (diag <- params.getDiagnostics.asScala) { val diagnosticMappings = generatedSources.valuesIterator .flatMap(_.iterator) .map { source => source.generated -> (source.reportingPath, source.wrapperParamsOpt) } .toMap val path = os.Path(Paths.get(new URI(params.getTextDocument.getUri)).toAbsolutePath) val (updatedPath, updatedDiag) = postProcessDiagnostic(path, diag, diagnosticMappings) .getOrElse((Right(path), diag)) if (keepDiagnostics) diagnostics0 += updatedPath -> updatedDiag ConsoleBloopBuildClient.printFileDiagnostic(logger, updatedPath, updatedDiag) } } override def onBuildLogMessage(params: bsp4j.LogMessageParams): Unit = { logger.debug("Received onBuildLogMessage from bloop: " + params) val prefix = params.getType match { case bsp4j.MessageType.ERROR => "Error: " case bsp4j.MessageType.WARNING => "Warning: " case bsp4j.MessageType.INFO => "" case bsp4j.MessageType.LOG => "" // discard those by default? } logger.message(prefix + params.getMessage) } override def onBuildShowMessage(params: bsp4j.ShowMessageParams): Unit = logger.debug("Received onBuildShowMessage from bloop: " + params) override def onBuildTargetDidChange(params: bsp4j.DidChangeBuildTarget): Unit = logger.debug("Received onBuildTargetDidChange from bloop: " + params) override def onBuildTaskStart(params: bsp4j.TaskStartParams): Unit = { logger.debug("Received onBuildTaskStart from bloop: " + params) for (msg <- Option(params.getMessage) if !msg.contains(" no-op compilation")) { printedStart = true val msg0 = if (params.getDataKind == "compile-task") s"Compiling $projectName" else msg logger.message(gray + msg0 + reset) } } override def onBuildTaskProgress(params: bsp4j.TaskProgressParams): Unit = logger.debug("Received onBuildTaskProgress from bloop: " + params) override def onBuildTaskFinish(params: bsp4j.TaskFinishParams): Unit = { logger.debug("Received onBuildTaskFinish from bloop: " + params) if (printedStart) for (msg <- Option(params.getMessage)) { val msg0 = if (params.getDataKind == "compile-report") params.getStatus match { case bsp4j.StatusCode.OK => s"Compiled $projectName" case bsp4j.StatusCode.ERROR => s"Error compiling $projectName" case bsp4j.StatusCode.CANCELLED => s"Compilation cancelled$projectNameSuffix" } else msg logger.message(gray + msg0 + reset) } } def clear(): Unit = { generatedSources.clear() diagnostics0.clear() printedStart = false } } object ConsoleBloopBuildClient { private val gray = ScalaCliConsole.GRAY private val reset = Console.RESET private val red = Console.RED private val yellow = Console.YELLOW def diagnosticPrefix(severity: bsp4j.DiagnosticSeverity): String = severity match { case bsp4j.DiagnosticSeverity.ERROR => s"[${red}error$reset] " case bsp4j.DiagnosticSeverity.WARNING => s"[${yellow}warn$reset] " case bsp4j.DiagnosticSeverity.INFORMATION => "[info] " case bsp4j.DiagnosticSeverity.HINT => s"[${yellow}hint$reset] " } def diagnosticPrefix(severity: Severity): String = diagnosticPrefix(severity.toBsp4j) def printFileDiagnostic( logger: Logger, path: Either[String, os.Path], diag: bsp4j.Diagnostic ): Unit = { val prefix = diagnosticPrefix(diag.getSeverity) val line = (diag.getRange.getStart.getLine + 1).toString + ":" val col = (diag.getRange.getStart.getCharacter + 1).toString val msgIt = diag.getMessage.linesIterator val path0 = path match { case Left(source) => source case Right(p) if p.startsWith(Os.pwd) => "." + File.separator + p.relativeTo(Os.pwd).toString case Right(p) => p.toString } logger.error(s"$prefix$path0:$line$col") for (line <- msgIt) logger.error(prefix + line) val codeOpt = { val lineOpt = if (diag.getRange.getStart.getLine == diag.getRange.getEnd.getLine) Option(diag.getRange.getStart.getLine) else None for { line <- lineOpt p <- path.toOption lines = try os.read.lines(p) catch case e: NoSuchFileException => logger.message(s"File not found: $p") logger.error(e.getMessage) Nil line <- lines.lift(line) } yield line } for (code <- codeOpt) code.linesIterator.map(prefix + _).foreach(logger.error) val canPrintUnderline = diag.getRange.getStart.getLine == diag.getRange.getEnd.getLine && diag.getRange.getStart.getCharacter != null && diag.getRange.getEnd.getCharacter != null && codeOpt.nonEmpty if (canPrintUnderline) { val len = math.max(1, diag.getRange.getEnd.getCharacter - diag.getRange.getStart.getCharacter) logger.error( prefix + " " * diag.getRange.getStart.getCharacter + "^" * len ) } } def printOtherDiagnostic( logger: Logger, message: String, severity: Severity, positions: Seq[Position] ): Unit = { val isWarningOrError = true if (isWarningOrError) { val msgIt = message.linesIterator val prefix = diagnosticPrefix(severity) logger.message(prefix + (if (msgIt.hasNext) " " + msgIt.next() else "")) msgIt.foreach(line => logger.message(prefix + line)) positions.foreach { case Position.Bloop(bloopJavaPath) => val bloopOutputPrefix = s"[current bloop jvm] " logger.message(prefix + bloopOutputPrefix + bloopJavaPath) logger.message(prefix + " " * bloopOutputPrefix.length + "^" * bloopJavaPath.length()) case pos => logger.message(prefix + pos.render()) } } } } ================================================ FILE: modules/build/src/main/scala/scala/build/CrossBuildParams.scala ================================================ package scala.build import dependency.ScalaParameters import scala.build.internal.Constants import scala.build.options.BuildOptions case class CrossBuildParams(scalaVersion: String, platform: String) { def asString: String = s"Scala $scalaVersion, $platform" } object CrossBuildParams { def apply(buildOptions: BuildOptions): CrossBuildParams = new CrossBuildParams( scalaVersion = buildOptions.scalaOptions.scalaVersion .map(_.asString) .getOrElse(Constants.defaultScalaVersion), platform = buildOptions.platform.value.repr ) def apply(scalaParams: Option[ScalaParameters], buildOptions: BuildOptions): CrossBuildParams = new CrossBuildParams( scalaVersion = scalaParams.map(_.scalaVersion) .orElse(buildOptions.scalaOptions.scalaVersion.map(_.asString)) .getOrElse(Constants.defaultScalaVersion), platform = buildOptions.platform.value.repr ) } ================================================ FILE: modules/build/src/main/scala/scala/build/CrossKey.scala ================================================ package scala.build import scala.build.options.{BuildOptions, MaybeScalaVersion, Platform, Scope} final case class CrossKey( optionsKey: Option[BuildOptions.CrossKey], scope: Scope ) { def scalaVersion: MaybeScalaVersion = optionsKey .map(k => MaybeScalaVersion(k.scalaVersion)) .getOrElse(MaybeScalaVersion.none) def platform: Option[Platform] = optionsKey.map(_.platform) } ================================================ FILE: modules/build/src/main/scala/scala/build/CrossSources.scala ================================================ package scala.build import scala.build.EitherCps.{either, value} import scala.build.Ops.* import scala.build.errors.{ BuildException, CompositeBuildException, ExcludeDefinitionError, MalformedDirectiveError, Severity, UsingFileFromUriError } import scala.build.input.* import scala.build.input.ElementsUtils.* import scala.build.internal.Constants import scala.build.internal.util.{RegexUtils, WarningMessages} import scala.build.options.{ BuildOptions, BuildRequirements, MaybeScalaVersion, Scope, SuppressWarningOptions, WithBuildRequirements } import scala.build.preprocessing.* import scala.util.Try import scala.util.chaining.* /** Information gathered from preprocessing command inputs - sources (including unwrapped scripts) * and build options from using directives * * @param paths * paths and realtive paths to sources on disk, wrapped in their build requirements * @param inMemory * in memory sources (e.g. snippets) wrapped in their build requirements * @param defaultMainClass * @param resourceDirs * @param buildOptions * build options from sources * @param unwrappedScripts * in memory script sources, their code must be wrapped before compiling */ final case class CrossSources( paths: Seq[WithBuildRequirements[(os.Path, os.RelPath)]], inMemory: Seq[WithBuildRequirements[Sources.InMemory]], defaultMainElemPath: Option[os.Path], resourceDirs: Seq[WithBuildRequirements[os.Path]], buildOptions: Seq[WithBuildRequirements[BuildOptions]], unwrappedScripts: Seq[WithBuildRequirements[Sources.UnwrappedScript]] ) { def sharedOptions(baseOptions: BuildOptions): BuildOptions = buildOptions .filter(_.requirements.isEmpty) .map(_.value) .foldLeft(baseOptions)(_.orElse(_)) private def needsScalaVersion = paths.exists(_.needsScalaVersion) || inMemory.exists(_.needsScalaVersion) || resourceDirs.exists(_.needsScalaVersion) || buildOptions.exists(_.needsScalaVersion) def scopedSources(baseOptions: BuildOptions): Either[BuildException, ScopedSources] = either { val sharedOptions0 = sharedOptions(baseOptions) // FIXME Not 100% sure the way we compute the intermediate and final BuildOptions // is consistent (we successively filter out / retain options to compute a scala // version and platform, which might not be the version and platform of the final // BuildOptions). val crossSources0 = if (needsScalaVersion) { val retainedScalaVersion = value(sharedOptions0.scalaParams) .map(p => MaybeScalaVersion(p.scalaVersion)) .getOrElse(MaybeScalaVersion.none) val buildOptionsWithScalaVersion = buildOptions .flatMap(_.withScalaVersion(retainedScalaVersion).toSeq) .filter(_.requirements.isEmpty) .map(_.value) .foldLeft(sharedOptions0)(_.orElse(_)) val platform = buildOptionsWithScalaVersion.platform copy( paths = paths .flatMap(_.withScalaVersion(retainedScalaVersion).toSeq) .flatMap(_.withPlatform(platform.value).toSeq), inMemory = inMemory .flatMap(_.withScalaVersion(retainedScalaVersion).toSeq) .flatMap(_.withPlatform(platform.value).toSeq), resourceDirs = resourceDirs .flatMap(_.withScalaVersion(retainedScalaVersion).toSeq) .flatMap(_.withPlatform(platform.value).toSeq), buildOptions = buildOptions .filter(!_.requirements.isEmpty) .flatMap(_.withScalaVersion(retainedScalaVersion).toSeq) .flatMap(_.withPlatform(platform.value).toSeq), unwrappedScripts = unwrappedScripts .flatMap(_.withScalaVersion(retainedScalaVersion).toSeq) .flatMap(_.withPlatform(platform.value).toSeq) ) } else { val platform = sharedOptions0.platform copy( paths = paths .flatMap(_.withPlatform(platform.value).toSeq), inMemory = inMemory .flatMap(_.withPlatform(platform.value).toSeq), resourceDirs = resourceDirs .flatMap(_.withPlatform(platform.value).toSeq), buildOptions = buildOptions .filter(!_.requirements.isEmpty) .flatMap(_.withPlatform(platform.value).toSeq), unwrappedScripts = unwrappedScripts .flatMap(_.withPlatform(platform.value).toSeq) ) } val defaultScope: Scope = Scope.Main ScopedSources( crossSources0.paths.map(_.scopedValue(defaultScope)), crossSources0.inMemory.map(_.scopedValue(defaultScope)), defaultMainElemPath, crossSources0.resourceDirs.map(_.scopedValue(defaultScope)), crossSources0.buildOptions.map(_.scopedValue(defaultScope)), crossSources0.unwrappedScripts.map(_.scopedValue(defaultScope)) ) } } object CrossSources { private def withinTestSubDirectory(p: ScopePath, inputs: Inputs): Boolean = p.root.exists { path => val fullPath = path / p.subPath inputs.elements.exists { case Directory(path) => // Is this file subdirectory of given dir and if we have a subdiretory 'test' on the way fullPath.startsWith(path) && fullPath.relativeTo(path).segments.contains("test") case _ => false } } /** @return * a CrossSources and Inputs which contains element processed from using directives */ def forInputs( inputs: Inputs, preprocessors: Seq[Preprocessor], logger: Logger, suppressWarningOptions: SuppressWarningOptions, exclude: Seq[Positioned[String]] = Nil, maybeRecoverOnError: BuildException => Option[BuildException] = e => Some(e), download: BuildOptions.Download = BuildOptions.Download.notSupported )(using ScalaCliInvokeData): Either[BuildException, (CrossSources, Inputs)] = either { def preprocessSources(elems: Seq[SingleElement]) : Either[BuildException, Seq[PreprocessedSource]] = elems .map { elem => preprocessors .iterator .flatMap(p => p.preprocess( elem, logger, maybeRecoverOnError, inputs.allowRestrictedFeatures, suppressWarningOptions ).iterator ) .take(1) .toList .headOption .getOrElse(Right(Nil)) // FIXME Warn about unprocessed stuff? } .sequence .left.map(CompositeBuildException(_)) .map(_.flatten) val flattenedInputs = inputs.flattened() val allExclude = { // supports only one exclude directive in one source file, which should be the project file. val projectScalaFileOpt = flattenedInputs.collectFirst { case f: ProjectScalaFile => f } val excludeFromProjectFile = value(preprocessSources(projectScalaFileOpt.toSeq)) .flatMap(_.options).flatMap(_.internal.exclude) exclude ++ excludeFromProjectFile } val preprocessedInputFromArgs: Seq[PreprocessedSource] = value( preprocessSources(value(excludeSources(flattenedInputs, inputs.workspace, allExclude))) ) val sourcesFromDirectives = preprocessedInputFromArgs .flatMap(_.options) .flatMap(_.internal.extraSourceFiles) .distinct val inputsElemFromDirectives: Seq[SingleElement] = value(resolveInputsFromSources(sourcesFromDirectives, inputs.enableMarkdown, download)) val preprocessedSourcesFromDirectives: Seq[PreprocessedSource] = value(preprocessSources(inputsElemFromDirectives.pipe(elements => value(excludeSources(elements, inputs.workspace, allExclude)) ))) warnAboutChainedUsingFileDirectives(preprocessedSourcesFromDirectives, logger) val allInputs = inputs.add(inputsElemFromDirectives).pipe(inputs => val filteredElements = value(excludeSources(inputs.elements, inputs.workspace, allExclude)) inputs.withElements(elements = filteredElements) ) val preprocessedSources: Seq[PreprocessedSource] = (preprocessedInputFromArgs ++ preprocessedSourcesFromDirectives).distinct .pipe { sources => val validatedSources: Seq[PreprocessedSource] = value(validateExcludeDirectives(sources, allInputs.workspace)) val distinctSources = validatedSources.distinctBy(_.distinctPathOrSource) val diff = validatedSources.diff(distinctSources) if diff.nonEmpty then val diffString = diff.map(_.distinctPathOrSource).mkString(s"${System.lineSeparator} ") logger.message( s"""[${Console.YELLOW}warn${Console.RESET}] Skipped duplicate sources: | $diffString""".stripMargin ) distinctSources } logger.flushExperimentalWarnings val scopedRequirements = preprocessedSources.flatMap(_.scopedRequirements) val scopedRequirementsByRoot = scopedRequirements.groupBy(_.path.root) def baseReqs(path: ScopePath): BuildRequirements = { val fromDirectives = scopedRequirementsByRoot .getOrElse(path.root, Nil) .flatMap(_.valueFor(path).toSeq) .foldLeft(BuildRequirements())(_.orElse(_)) // Scala CLI treats all `.test.scala` files tests as well as // files from within `test` subdirectory from provided input directories // If file has `using target ` directive this take precendeces. if ( fromDirectives.scope.isEmpty && (path.subPath.last.endsWith(".test.scala") || withinTestSubDirectory(path, allInputs)) ) fromDirectives.copy(scope = Some(BuildRequirements.ScopeRequirement(Scope.Test))) else fromDirectives } val buildOptions: Seq[WithBuildRequirements[BuildOptions]] = (for { preprocessedSource <- preprocessedSources opts <- preprocessedSource.options.toSeq if opts != BuildOptions() || preprocessedSource.optionsWithTargetRequirements.nonEmpty } yield { val baseReqs0 = baseReqs(preprocessedSource.scopePath) preprocessedSource.optionsWithTargetRequirements :+ WithBuildRequirements( preprocessedSource.requirements.fold(baseReqs0)(_.orElse(baseReqs0)), opts ) }).flatten val defaultMainElemPath = for { defaultMainElem <- allInputs.defaultMainClassElement } yield defaultMainElem.path val pathsWithDirectivePositions : Seq[(WithBuildRequirements[(os.Path, os.RelPath)], Option[Position.File])] = preprocessedSources.collect { case d: PreprocessedSource.OnDisk => val baseReqs0 = baseReqs(d.scopePath) WithBuildRequirements( d.requirements.fold(baseReqs0)(_.orElse(baseReqs0)), (d.path, d.path.relativeTo(allInputs.workspace)) ) -> d.directivesPositions } val inMemoryWithDirectivePositions : Seq[(WithBuildRequirements[Sources.InMemory], Option[Position.File])] = preprocessedSources.collect { case m: PreprocessedSource.InMemory => val baseReqs0 = baseReqs(m.scopePath) WithBuildRequirements( m.requirements.fold(baseReqs0)(_.orElse(baseReqs0)), Sources.InMemory(m.originalPath, m.relPath, m.content, m.wrapperParamsOpt) ) -> m.directivesPositions } val unwrappedScriptsWithDirectivePositions : Seq[(WithBuildRequirements[Sources.UnwrappedScript], Option[Position.File])] = preprocessedSources.collect { case m: PreprocessedSource.UnwrappedScript => val baseReqs0 = baseReqs(m.scopePath) WithBuildRequirements( m.requirements.fold(baseReqs0)(_.orElse(baseReqs0)), Sources.UnwrappedScript(m.originalPath, m.relPath, m.wrapScriptFun) ) -> m.directivesPositions } val resourceDirs: Seq[WithBuildRequirements[os.Path]] = resolveResourceDirs(allInputs, preprocessedSources) lazy val allPathsWithDirectivesByScope: Map[Scope, Seq[(os.Path, Position.File)]] = (pathsWithDirectivePositions ++ inMemoryWithDirectivePositions ++ unwrappedScriptsWithDirectivePositions) .flatMap { (withBuildRequirements, directivesPositions) => val scope = withBuildRequirements.scopedValue(Scope.Main).scope val path: os.Path = withBuildRequirements.value match case im: Sources.InMemory => im.originalPath match case Right((_, p: os.Path)) => p case _ => inputs.workspace / im.generatedRelPath case us: Sources.UnwrappedScript => us.originalPath match case Right((_, p: os.Path)) => p case _ => inputs.workspace / us.generatedRelPath case (p: os.Path, _) => p directivesPositions.map((path, scope, _)) } .groupBy((_, scope, _) => scope) .view .mapValues(_.map((path, _, directivesPositions) => path -> directivesPositions)) .toMap lazy val anyScopeHasMultipleSourcesWithDirectives = Scope.all.exists(allPathsWithDirectivesByScope.get(_).map(_.length).getOrElse(0) > 1) val shouldSuppressWarning = suppressWarningOptions.suppressDirectivesInMultipleFilesWarning.getOrElse(false) if !shouldSuppressWarning && anyScopeHasMultipleSourcesWithDirectives then { val projectFilePath = inputs.elements.projectSettingsFiles.headOption match case Some(s) => s.path case _ => inputs.workspace / Constants.projectFileName allPathsWithDirectivesByScope .values .flatten .filter((path, _) => ScopePath.fromPath(path) != ScopePath.fromPath(projectFilePath)) .pipe { pathsToReport => val diagnosticMessage = WarningMessages .directivesInMultipleFilesWarning(projectFilePath.toString) val cliFriendlyMessage = WarningMessages.directivesInMultipleFilesWarning( projectFilePath.toString, pathsToReport.map(_._2.render()) ) logger.cliFriendlyDiagnostic( message = diagnosticMessage, cliFriendlyMessage = cliFriendlyMessage, positions = pathsToReport.map(_._2).toSeq ) } } val paths = pathsWithDirectivePositions.map(_._1) val inMemory = inMemoryWithDirectivePositions.map(_._1) val unwrappedScripts = unwrappedScriptsWithDirectivePositions.map(_._1) val crossSources = CrossSources( paths, inMemory, defaultMainElemPath, resourceDirs, buildOptions, unwrappedScripts ) crossSources -> allInputs } extension (uri: java.net.URI) def asString: String = java.net.URI( uri.getScheme, uri.getAuthority, uri.getPath, null, uri.getFragment ).toString /** @return * the resource directories that should be added to the classpath */ private def resolveResourceDirs( allInputs: Inputs, preprocessedSources: Seq[PreprocessedSource] ): Seq[WithBuildRequirements[os.Path]] = { val fromInputs = allInputs.elements .collect { case r: ResourceDirectory => WithBuildRequirements(BuildRequirements(), r.path) } val fromSources = preprocessedSources.flatMap(_.options) .flatMap(_.classPathOptions.resourcesDir) .map(r => WithBuildRequirements(BuildRequirements(), r)) val fromSourcesWithRequirements = preprocessedSources .flatMap(_.optionsWithTargetRequirements) .flatMap(_.map(_.classPathOptions.resourcesDir).flatten) fromInputs ++ fromSources ++ fromSourcesWithRequirements } private def downloadFile(download: BuildOptions.Download)(pUri: Positioned[java.net.URI]) = download(pUri.value.toString).left.map( new UsingFileFromUriError(pUri.value, pUri.positions, _) ).map(content => Seq(Virtual(pUri.value.asString, content)) ) type CodeFile = os.Path | java.net.URI private def resolveInputsFromSources( sources: Seq[Positioned[CodeFile]], enableMarkdown: Boolean, download: BuildOptions.Download ) = val links = sources.collect { case Positioned(pos, value: java.net.URI) => Positioned(pos, value) } val paths = sources.collect { case Positioned(pos, value: os.Path) => Positioned(pos, value) } (resolveInputsFromPath(paths, enableMarkdown) ++ links.map(downloadFile(download))).sequence .left.map(CompositeBuildException(_)) .map(_.flatten) private def resolveInputsFromPath(sources: Seq[Positioned[os.Path]], enableMarkdown: Boolean) = sources.map { source => val sourcePath = source.value lazy val dir = sourcePath / os.up lazy val subPath = sourcePath.subRelativeTo(dir) if (os.isDir(sourcePath)) Right(Directory(sourcePath).singleFilesFromDirectory(enableMarkdown)) else if (sourcePath == os.sub / Constants.projectFileName) Right(Seq(ProjectScalaFile(dir, subPath))) else if (sourcePath.ext == "scala") Right(Seq(SourceScalaFile(dir, subPath))) else if (sourcePath.isScript) Right(Seq(Script(dir, subPath, None))) else if (sourcePath.ext == "java") Right(Seq(JavaFile(dir, subPath))) else if (sourcePath.ext == "jar") Right(Seq(JarFile(dir, subPath))) else if (sourcePath.ext == "md") Right(Seq(MarkdownFile(dir, subPath))) else { val msg = if (os.exists(sourcePath)) s"$sourcePath: unrecognized source type (expected .scala, .sc, .java extension or directory) in using directive." else s"$sourcePath: not found path defined in using directive." Left(new MalformedDirectiveError(msg, source.positions)) } } /** Filters out the sources from the input sequence based on the provided 'exclude' patterns. The * exclude patterns can be absolute paths, relative paths, or glob patterns. * * @throws BuildException * If multiple 'exclude' patterns are defined across the input sources. */ private def excludeSources[E <: Element]( elements: Seq[E], workspaceDir: os.Path, exclude: Seq[Positioned[String]] ): Either[BuildException, Seq[E]] = either { val excludePatterns = exclude.map(_.value).flatMap { p => val maybeRelPath = Try(os.RelPath(p)).toOption maybeRelPath match { case Some(relPath) if os.isDir(workspaceDir / relPath) => // exclude relative directory paths, add * to exclude all files in the directory Seq(p, (workspaceDir / relPath / "*").toString) case Some(relPath) => Seq(p, (workspaceDir / relPath).toString) // exclude relative paths case None => Seq(p) } } def isSourceIncluded(path: String, excludePatterns: Seq[String]): Boolean = excludePatterns .forall(pattern => !RegexUtils.globPattern(pattern).matcher(path).matches()) elements.filter { case e: OnDisk => isSourceIncluded(e.path.toString, excludePatterns) case _ => true } } /** Validates that exclude directives are defined only in the one source. */ def validateExcludeDirectives( sources: Seq[PreprocessedSource], workspaceDir: os.Path ): Either[BuildException, Seq[PreprocessedSource]] = { val excludePositions = for { source <- sources.flatMap(_.options) exclude <- source.internal.exclude position <- exclude.positions } yield position val expectedProjectFilePath = workspaceDir / Constants.projectFileName val singleSourceAtProject = excludePositions.forall { case Position.File(Left(s), _, _, _) => workspaceDir / s == expectedProjectFilePath case Position.File(Right(p), _, _, _) => p == expectedProjectFilePath case _ => false } if (singleSourceAtProject) Right(sources) else Left(new ExcludeDefinitionError(excludePositions, expectedProjectFilePath)) } /** When a source file added by a `using file` directive, itself, contains `using file` directives * there should be a warning printed that transitive `using file` directives are not supported. */ def warnAboutChainedUsingFileDirectives( sourcesAddedWithDirectives: Seq[PreprocessedSource], logger: Logger ): Unit = for { additionalSource <- sourcesAddedWithDirectives buildOptions <- additionalSource.options transitiveAdditionalSource <- buildOptions.internal.extraSourceFiles } do logger.diagnostic( WarningMessages.chainingUsingFileDirective, Severity.Warning, transitiveAdditionalSource.positions ) } ================================================ FILE: modules/build/src/main/scala/scala/build/Directories.scala ================================================ package scala.build import coursier.paths.shaded.dirs.ProjectDirectories import coursier.paths.shaded.dirs.impl.Windows import coursier.paths.shaded.dirs.jni.WindowsJni import java.util.function.Supplier import scala.build.errors.ConfigDbException import scala.build.internals.EnvVar import scala.cli.config.ConfigDb import scala.util.Properties trait Directories { def localRepoDir: os.Path def binRepoDir: os.Path def completionsDir: os.Path def virtualProjectsDir: os.Path def bspSocketDir: os.Path def bloopDaemonDir: os.Path def bloopWorkingDir: os.Path def secretsDir: os.Path def cacheDir: os.Path final def dbPath: os.Path = EnvVar.ScalaCli.config.valueOpt .filter(_.trim.nonEmpty) .map(os.Path(_, os.pwd)) .getOrElse(secretsDir / Directories.defaultDbFileName) lazy val configDb = ConfigDb.open(dbPath.toNIO).left.map(ConfigDbException(_)) } object Directories { def defaultDbFileName: String = "config.json" final case class OsLocations(projDirs: ProjectDirectories) extends Directories { lazy val localRepoDir: os.Path = os.Path(projDirs.cacheDir, Os.pwd) / "local-repo" lazy val binRepoDir: os.Path = os.Path(localRepoDir, Os.pwd) / "bin" lazy val completionsDir: os.Path = os.Path(projDirs.dataLocalDir, Os.pwd) / "completions" lazy val virtualProjectsDir: os.Path = os.Path(projDirs.cacheDir, Os.pwd) / "virtual-projects" lazy val bspSocketDir: os.Path = // FIXME I would have preferred to use projDirs.dataLocalDir, but it seems named socket // support, or name sockets in general, aren't fine with it. os.Path(projDirs.cacheDir, Os.pwd) / "bsp-sockets" lazy val bloopDaemonDir: os.Path = bloopWorkingDir / "daemon" lazy val bloopWorkingDir: os.Path = { val baseDir = if (Properties.isMac) projDirs.cacheDir else projDirs.dataLocalDir os.Path(baseDir, Os.pwd) / "bloop" } lazy val secretsDir: os.Path = os.Path(projDirs.dataLocalDir, Os.pwd) / "secrets" lazy val cacheDir: os.Path = os.Path(projDirs.cacheDir, os.pwd) } final case class SubDir(dir: os.Path) extends Directories { lazy val localRepoDir: os.Path = dir / "cache" / "local-repo" lazy val binRepoDir: os.Path = localRepoDir / "bin" lazy val completionsDir: os.Path = dir / "data-local" / "completions" lazy val virtualProjectsDir: os.Path = dir / "cache" / "virtual-projects" lazy val bspSocketDir: os.Path = dir / "data-local" / "bsp-sockets" lazy val bloopDaemonDir: os.Path = bloopWorkingDir / "daemon" lazy val bloopWorkingDir: os.Path = dir / "data-local" / "bloop" lazy val secretsDir: os.Path = dir / "data-local" / "secrets" lazy val cacheDir: os.Path = dir / "cache" } def default(): Directories = { val windows: Supplier[Windows] = if coursier.paths.Util.useJni() then WindowsJni.getJdkAwareSupplier else Windows.getDefaultSupplier OsLocations(ProjectDirectories.from(null, null, "ScalaCli", windows)) } def under(dir: os.Path): Directories = SubDir(dir) lazy val directories: Directories = EnvVar.ScalaCli.home.valueOpt.filter(_.trim.nonEmpty) match { case None => scala.build.Directories.default() case Some(homeDir) => val homeDir0 = os.Path(homeDir, Os.pwd) scala.build.Directories.under(homeDir0) } } ================================================ FILE: modules/build/src/main/scala/scala/build/GeneratedSource.scala ================================================ package scala.build import scala.build.internal.WrapperParams /** Represents a source that's not originally in the user's workspace, yet it's a part of the * project. It can either be synthetically generated by Scala CLI, e.g. BuildInfo or just modified, * e.g. script wrappers * * @param generated * path to the file created by Scala CLI * @param reportingPath * the origin of the source: * - Left(String): there's no path that corresponds to the source it may be a snippet or a gist * etc. * - Right(os.Path): this source has been generated based on a file at this path * @param wrapperParamsOpt * if the generated source is a script wrapper then the params are present here */ final case class GeneratedSource( generated: os.Path, reportingPath: Either[String, os.Path], wrapperParamsOpt: Option[WrapperParams] ) ================================================ FILE: modules/build/src/main/scala/scala/build/LocalRepo.scala ================================================ package scala.build import coursier.paths.Util import java.io.{BufferedInputStream, Closeable} import java.nio.channels.{FileChannel, FileLock} import java.nio.charset.StandardCharsets import java.nio.file.{Path, StandardOpenOption} import scala.build.internal.Constants import scala.build.internal.zip.WrappedZipInputStream object LocalRepo { private def resourcePath = Constants.localRepoResourcePath private def using[S <: Closeable, T](is: => S)(f: S => T): T = { var is0 = Option.empty[S] try { is0 = Some(is) f(is0.get) } finally if (is0.nonEmpty) is0.get.close() } private def extractZip(zis: WrappedZipInputStream, dest: os.Path): Unit = { val it = zis.entries() while (it.hasNext) { val ent = it.next() if (!ent.isDirectory) { val content = zis.readAllBytes() zis.closeEntry() os.write( dest / ent.getName.split('/').toSeq, content, createFolders = true ) } } } private def entryContent(zis: WrappedZipInputStream, entryPath: String): Option[Array[Byte]] = { val it = zis.entries().dropWhile(ent => ent.isDirectory || ent.getName != entryPath) if (it.hasNext) { val ent = it.next() assert(ent.getName == entryPath) val content = zis.readAllBytes() zis.closeEntry() Some(content) } else None } def localRepo( baseDir: os.Path, logger: Logger, loader: ClassLoader = Thread.currentThread().getContextClassLoader ): Option[String] = { val archiveUrl = loader.getResource(resourcePath) logger.debug(s"archive url: $archiveUrl") if archiveUrl == null then None else { val version = using(archiveUrl.openStream()) { is => using(WrappedZipInputStream.create(new BufferedInputStream(is))) { zis => val b = entryContent(zis, "version").getOrElse { sys.error(s"Malformed local repo JAR $archiveUrl (no version file)") } new String(b, StandardCharsets.UTF_8) } } val repoDir = baseDir / version logger.debug(s"repo dir: $repoDir") if !os.exists(repoDir) then withLock((repoDir / os.up).toNIO, version) { // Post-lock validation: Recheck repository directory existence to handle // potential race conditions between initial check and lock acquisition if !os.exists(repoDir) then val tmpRepoDir = repoDir / os.up / s".$version.tmp" logger.debug(s"tmp repo dir: $tmpRepoDir") os.remove.all(tmpRepoDir) using(archiveUrl.openStream()) { is => using(WrappedZipInputStream.create(new BufferedInputStream(is))) { zis => extractZip(zis, tmpRepoDir) } } os.move(tmpRepoDir, repoDir) } val repo = "ivy:" + repoDir.toNIO.toUri.toASCIIString + "/[defaultPattern]" logger.debug(s"local repo (ivy): $repo") Some(repo) } } private val intraProcessLock = new Object private def withLock[T](dir: Path, id: String)(f: => T): T = intraProcessLock.synchronized { val lockFile = dir.resolve(s".lock-$id"); Util.createDirectories(lockFile.getParent) var channel: FileChannel = null var lock: FileLock = null try { channel = FileChannel.open( lockFile, StandardOpenOption.CREATE, StandardOpenOption.WRITE ) lock = channel.lock() f } finally { if (lock != null) lock.release() if (channel != null) channel.close() } } } ================================================ FILE: modules/build/src/main/scala/scala/build/PersistentDiagnosticLogger.scala ================================================ package scala.build import bloop.rifle.BloopRifleLogger import org.scalajs.logging.Logger as ScalaJsLogger import java.io.PrintStream import scala.build.errors.{BuildException, Diagnostic} import scala.build.internals.FeatureType import scala.scalanative.build as sn /** Used to collect and send diagnostics to the build client when operating as a BSP */ class PersistentDiagnosticLogger(parent: Logger) extends Logger { private val diagBuilder = List.newBuilder[Diagnostic] def diagnostics = diagBuilder.result() def error(message: String): Unit = parent.error(message) // TODO Use macros for log and debug calls to have zero cost when verbosity <= 0 def message(message: => String): Unit = parent.message(message) def log(s: => String): Unit = parent.log(s) def log(s: => String, debug: => String): Unit = parent.log(s, debug) def debug(s: => String): Unit = parent.debug(s) def log(diagnostics: Seq[Diagnostic]): Unit = { parent.log(diagnostics) diagBuilder ++= diagnostics } def log(ex: BuildException): Unit = parent.log(ex) def debug(ex: BuildException): Unit = parent.debug(ex) def exit(ex: BuildException): Nothing = parent.exit(ex) def coursierLogger(printBefore: String): coursier.cache.CacheLogger = parent.coursierLogger(printBefore) def bloopRifleLogger: BloopRifleLogger = parent.bloopRifleLogger def scalaJsLogger: ScalaJsLogger = parent.scalaJsLogger def scalaNativeTestLogger: sn.Logger = parent.scalaNativeTestLogger def scalaNativeCliInternalLoggerOptions: List[String] = parent.scalaNativeCliInternalLoggerOptions def compilerOutputStream: PrintStream = parent.compilerOutputStream def verbosity: Int = parent.verbosity def experimentalWarning(featureName: String, featureType: FeatureType): Unit = parent.experimentalWarning(featureName, featureType) def flushExperimentalWarnings: Unit = parent.flushExperimentalWarnings } ================================================ FILE: modules/build/src/main/scala/scala/build/Project.scala ================================================ package scala.build import _root_.bloop.config.{Config as BloopConfig, ConfigCodecs as BloopCodecs} import _root_.coursier.{Dependency as CsDependency, core as csCore, util as csUtil} import com.github.plokhotnyuk.jsoniter_scala.core.writeToArray as writeAsJsonToArray import coursier.core.Classifier import java.io.ByteArrayOutputStream import java.nio.charset.StandardCharsets import java.nio.file.Path import java.util.Arrays import scala.build.options.{ScalacOpt, Scope, ShadowingSeq} final case class Project( workspace: os.Path, directory: os.Path, argsFilePath: os.Path, classesDir: os.Path, scaladocDir: os.Path, scalaCompiler: Option[ScalaCompilerParams], scalaJsOptions: Option[BloopConfig.JsConfig], scalaNativeOptions: Option[BloopConfig.NativeConfig], projectName: String, classPath: Seq[os.Path], sources: Seq[os.Path], resolution: Option[BloopConfig.Resolution], resourceDirs: Seq[os.Path], javaHomeOpt: Option[os.Path], scope: Scope, javacOptions: List[String] ) { import Project._ def bloopProject: BloopConfig.Project = { val platform = (scalaJsOptions, scalaNativeOptions) match { case (None, None) => val baseJvmConf = bloopJvmPlatform val home = javaHomeOpt.map(_.toNIO).orElse(baseJvmConf.config.home) baseJvmConf.copy(config = baseJvmConf.config.copy(home = home)) case (Some(jsConfig), _) => BloopConfig.Platform.Js(config = jsConfig, mainClass = None) case (_, Some(nativeConfig)) => BloopConfig.Platform.Native(config = nativeConfig, mainClass = None) } val scalaConfigOpt = scalaCompiler.map { scalaCompiler0 => bloopScalaConfig("org.scala-lang", "scala-compiler", scalaCompiler0.scalaVersion).copy( options = updateScalacOptions(scalaCompiler0.scalacOptions).map(_.value), jars = scalaCompiler0.compilerClassPath.map(_.toNIO).toList, bridgeJars = scalaCompiler0.bridgeJarsOpt.map(_.map(_.toNIO).toList) ) } baseBloopProject( projectName, directory.toNIO, (directory / ".bloop" / projectName).toNIO, classesDir.toNIO, scope ) .copy( workspaceDir = Some(workspace.toNIO), classpath = classPath.map(_.toNIO).toList, sources = sources.iterator.map(_.toNIO).toList, resources = Some(resourceDirs).filter(_.nonEmpty).map(_.iterator.map(_.toNIO).toList), platform = Some(platform), `scala` = scalaConfigOpt, java = Some(BloopConfig.Java(javacOptions)), resolution = resolution ) } def bloopFile: BloopConfig.File = BloopConfig.File(BloopConfig.File.LatestVersion, bloopProject) private def updateScalacOptions(scalacOptions: Seq[String]): List[ScalacOpt] = ShadowingSeq.from(scalacOptions.map(ScalacOpt(_))).values.map { l => // only look at the head, the tail is only values passed to it l.headOption match { case Some(opt) if opt.value.startsWith("-coverage-out:") => // actual -coverage-out: option val maybeRelativePath = opt.value.stripPrefix("-coverage-out:") val absolutePath = os.Path(maybeRelativePath, Os.pwd) ScalacOpt(s"-coverage-out:$absolutePath") +: l.tail case _ => // not a -coverage-out: option l } }.flatten.toList private def maybeUpdateInputs(logger: Logger): Boolean = { val dest = directory / ".bloop" / s"$projectName.inputs.txt" val onDiskOpt = if (os.exists(dest)) Some(os.read.bytes(dest)) else None val newContent = { val linesIt = if (sources.forall(_.startsWith(workspace))) sources.iterator.map(_.relativeTo(workspace).toString) else sources.iterator.map(_.toString) val it = linesIt.map(_ + System.lineSeparator()).map(_.getBytes(StandardCharsets.UTF_8)) val b = new ByteArrayOutputStream for (elem <- it) b.write(elem) b.toByteArray() } val doWrite = onDiskOpt.forall(onDisk => !Arrays.equals(onDisk, newContent)) if (doWrite) { logger.debug(s"Writing source file list in $dest") os.write.over(dest, newContent, createFolders = true) } else logger.debug(s"Source file list in $dest doesn't need updating") doWrite } def writeBloopFile(strictCheck: Boolean, logger: Logger): Boolean = { lazy val bloopFileContent = writeAsJsonToArray(bloopFile)(using BloopCodecs.codecFile) val dest = directory / ".bloop" / s"$projectName.json" val doWrite = if (strictCheck) !os.isFile(dest) || { logger.debug(s"Checking Bloop project in $dest") val currentContent = os.read.bytes(dest) !Arrays.equals(currentContent, bloopFileContent) } else maybeUpdateInputs(logger) || !os.isFile(dest) if (doWrite) { logger.debug(s"Writing bloop project in $dest") os.write.over(dest, bloopFileContent, createFolders = true) } else logger.debug(s"Bloop project in $dest doesn't need updating") doWrite } } object Project { def resolution( detailedArtifacts: Seq[(CsDependency, csCore.Publication, csUtil.Artifact, os.Path)] ): BloopConfig.Resolution = { val indices = detailedArtifacts .map { case (dep, _, _, _) => dep.moduleVersionConstraint } .map { case (m, vc) => m -> vc.asString } .zipWithIndex.toMap val modules = detailedArtifacts .groupBy(_._1.moduleVersionConstraint) .map { case ((m, vc), artifacts) => m -> vc.asString -> artifacts } .toVector .sortBy { case (modVer, _) => indices.getOrElse(modVer, Int.MaxValue) } .iterator .map { case ((mod, ver), values) => val artifacts = values.toList.map { case (_, pub, _, f) => val classifier = if (pub.classifier == Classifier.empty) None else Some(pub.classifier.value) BloopConfig.Artifact(pub.name, classifier, None, f.toNIO) } BloopConfig.Module(mod.organization.value, mod.name.value, ver, None, artifacts) } .toList BloopConfig.Resolution(modules) } private def setProjectTestConfig(p: BloopConfig.Project): BloopConfig.Project = p.copy( dependencies = List(p.name.stripSuffix("-test")), test = Some( BloopConfig.Test( frameworks = BloopConfig.TestFramework.DefaultFrameworks, options = BloopConfig.TestOptions.empty ) ), tags = Some(List("test")) ) private def baseBloopProject( name: String, directory: Path, out: Path, classesDir: Path, scope: Scope ): BloopConfig.Project = { val project = BloopConfig.Project( name = name, directory = directory, workspaceDir = None, sources = Nil, sourcesGlobs = None, sourceRoots = None, dependencies = Nil, classpath = Nil, out = out, classesDir = classesDir, resources = None, `scala` = None, java = None, sbt = None, test = None, platform = None, resolution = None, tags = Some(List("library")), sourceGenerators = None ) if (scope == Scope.Test) setProjectTestConfig(project) else project } private def bloopJvmPlatform: BloopConfig.Platform.Jvm = BloopConfig.Platform.Jvm( config = BloopConfig.JvmConfig(None, Nil), mainClass = None, runtimeConfig = None, classpath = None, resources = None ) private def bloopScalaConfig( organization: String, name: String, version: String ): BloopConfig.Scala = BloopConfig.Scala( organization = organization, name = name, version = version, options = Nil, jars = Nil, analysis = None, setup = None, bridgeJars = None ) } ================================================ FILE: modules/build/src/main/scala/scala/build/ReplArtifacts.scala ================================================ package scala.build import coursier.cache.FileCache import coursier.core.Repository import coursier.util.Task import dependency.* import java.io.File import scala.build.EitherCps.{either, value} import scala.build.errors.BuildException import scala.build.internal.CsLoggerUtil.* final case class ReplArtifacts( replArtifacts: Seq[(String, os.Path)], depArtifacts: Seq[(String, os.Path)], extraClassPath: Seq[os.Path], extraSourceJars: Seq[os.Path], replMainClass: String, replJavaOpts: Seq[String], addSourceJars: Boolean, includeExtraCpOnReplCp: Boolean = false ) { private lazy val fullExtraClassPath: Seq[os.Path] = if addSourceJars then extraClassPath ++ extraSourceJars else extraClassPath lazy val replClassPath: Seq[os.Path] = (if includeExtraCpOnReplCp then fullExtraClassPath ++ replArtifacts.map(_._2).distinct else replArtifacts.map(_._2)) .distinct lazy val depsClassPath: Seq[os.Path] = (fullExtraClassPath ++ depArtifacts.map(_._2)).distinct } object ReplArtifacts { // TODO In order to isolate more Ammonite dependencies, we'd need to get two class paths: // - a shared one, with ammonite-repl-api, ammonite-compiler, and dependencies // - an Ammonite-specific one, with the other ammonite JARs // Then, use the coursier-bootstrap library to generate a launcher creating to class loaders, // with each of those class paths, and run Ammonite with this launcher. // This requires to change this line in Ammonite, https://github.com/com-lihaoyi/Ammonite/blob/0f0d597f04e62e86cbf76d3bd16deb6965331470/amm/src/main/scala/ammonite/Main.scala#L99, // to // val contextClassLoader = classOf[ammonite.repl.api.ReplAPI].getClassLoader // so that only the first loader is exposed to users in Ammonite. def ammonite( scalaParams: ScalaParameters, ammoniteVersion: String, dependencies: Seq[AnyDependency], extraClassPath: Seq[os.Path], extraSourceJars: Seq[os.Path], extraRepositories: Seq[Repository], logger: Logger, cache: FileCache[Task], addScalapy: Option[String] ): Either[BuildException, ReplArtifacts] = either { val scalapyDeps = addScalapy.map(ver => dep"${Artifacts.scalaPyOrganization(ver)}::scalapy-core::$ver").toSeq val allDeps = dependencies ++ Seq(dep"com.lihaoyi:::ammonite:$ammoniteVersion") ++ scalapyDeps val replArtifacts = Artifacts.artifacts( allDeps.map(Positioned.none), extraRepositories, Some(scalaParams), logger, cache.withMessage(s"Downloading Ammonite $ammoniteVersion") ) val replSourceArtifacts = Artifacts.artifacts( allDeps.map(Positioned.none), extraRepositories, Some(scalaParams), logger, cache.withMessage(s"Downloading Ammonite $ammoniteVersion sources"), classifiersOpt = Some(Set("sources")) ) ReplArtifacts( replArtifacts = value(replArtifacts) ++ value(replSourceArtifacts), depArtifacts = Nil, // amm does not support a -cp option, deps are passed directly to Ammonite cp extraClassPath = extraClassPath, extraSourceJars = extraSourceJars, replMainClass = "ammonite.Main", replJavaOpts = Nil, addSourceJars = true, includeExtraCpOnReplCp = true // extra cp & source jars have to be passed directly to Ammonite cp ) } def default( scalaParams: ScalaParameters, dependencies: Seq[AnyDependency], extraClassPath: Seq[os.Path], logger: Logger, cache: FileCache[Task], repositories: Seq[Repository], addScalapy: Option[String], javaVersion: Int ): Either[BuildException, ReplArtifacts] = either { val isScala2 = scalaParams.scalaVersion.startsWith("2.") val firstNewReplNightly = "3.8.0-RC1-bin-20251101-389483e-NIGHTLY".coursierVersion val firstNewReplRc = "3.8.0-RC1".coursierVersion val firstNewReplStable = "3.8.0".coursierVersion val scalaCoursierVersion = scalaParams.scalaVersion.coursierVersion val shouldUseNewRepl = !isScala2 && ((scalaCoursierVersion >= firstNewReplNightly) || (scalaCoursierVersion >= firstNewReplRc) || scalaCoursierVersion >= firstNewReplStable) val replDeps = if isScala2 then Seq(dep"org.scala-lang:scala-compiler:${scalaParams.scalaVersion}") else if shouldUseNewRepl then Seq( dep"org.scala-lang::scala3-compiler:${scalaParams.scalaVersion}", dep"org.scala-lang::scala3-repl:${scalaParams.scalaVersion}" ) else Seq(dep"org.scala-lang::scala3-compiler:${scalaParams.scalaVersion}") val scalapyDeps = addScalapy.map(ver => dep"${Artifacts.scalaPyOrganization(ver)}::scalapy-core::$ver").toSeq val externalDeps = dependencies ++ scalapyDeps val replArtifacts: Seq[(String, os.Path)] = value { Artifacts.artifacts( replDeps.map(Positioned.none), repositories, Some(scalaParams), logger, cache.withMessage(s"Downloading Scala compiler ${scalaParams.scalaVersion}") ) } val depArtifacts: Seq[(String, os.Path)] = value { Artifacts.artifacts( externalDeps.map(Positioned.none), repositories, Some(scalaParams), logger, cache.withMessage(s"Downloading REPL dependencies") ) } val mainClass = if isScala2 then "scala.tools.nsc.MainGenericRunner" else "dotty.tools.repl.Main" val defaultReplJavaOpts = Seq("-Dscala.usejavacp=true") val jlineArtifacts = replArtifacts .map(_._2.toString) .filter(_.contains("jline")) val jlineJavaOpts: Seq[String] = if javaVersion >= 24 && jlineArtifacts.nonEmpty then { val modulePath = Seq("--module-path", jlineArtifacts.mkString(File.pathSeparator)) val remainingOpts = if isScala2 then Seq( "--add-modules", "org.jline", "--enable-native-access=org.jline" ) else Seq( "--add-modules", "org.jline.terminal", "--enable-native-access=org.jline.nativ" ) modulePath ++ remainingOpts } else Seq.empty val replJavaOpts = defaultReplJavaOpts ++ jlineJavaOpts ReplArtifacts( replArtifacts = replArtifacts, depArtifacts = depArtifacts, extraClassPath = extraClassPath, extraSourceJars = Nil, replMainClass = mainClass, replJavaOpts = replJavaOpts, addSourceJars = false ) } } ================================================ FILE: modules/build/src/main/scala/scala/build/ScalaCompilerParams.scala ================================================ package scala.build final case class ScalaCompilerParams( scalaVersion: String, scalaBinaryVersion: String, scalacOptions: Seq[String], compilerClassPath: Seq[os.Path], bridgeJarsOpt: Option[Seq[os.Path]] ) ================================================ FILE: modules/build/src/main/scala/scala/build/ScalafixArtifacts.scala ================================================ package scala.build import coursier.cache.FileCache import coursier.core.Repository import coursier.util.Task import dependency.* import org.apache.commons.compress.archivers.zip.ZipFile import java.io.{ByteArrayInputStream, IOException} import java.util.Properties import scala.build.EitherCps.{either, value} import scala.build.errors.{BuildException, ScalafixPropertiesError} import scala.build.internal.Constants import scala.build.internal.CsLoggerUtil.* final case class ScalafixArtifacts( scalafixJars: Seq[os.Path], toolsJars: Seq[os.Path] ) object ScalafixArtifacts { def artifacts( scalaVersion: String, externalRulesDeps: Seq[Positioned[AnyDependency]], extraRepositories: Seq[Repository], logger: Logger, cache: FileCache[Task] ): Either[BuildException, ScalafixArtifacts] = either { val scalafixProperties = value(fetchOrLoadScalafixProperties(extraRepositories, logger, cache)) val key = value(scalafixPropsKey(scalaVersion)) val fetchScalaVersion = scalafixProperties.getProperty(key) val scalafixDeps = Seq(dep"ch.epfl.scala:scalafix-cli_$fetchScalaVersion:${Constants.scalafixVersion}") val scalafix = value( Artifacts.artifacts( scalafixDeps.map(Positioned.none), extraRepositories, None, logger, cache.withMessage(s"Downloading scalafix-cli ${Constants.scalafixVersion}") ) ) val scalaParameters = // Scalafix for scala 3 uses 2.13-published community rules // https://github.com/scalacenter/scalafix/issues/2041 if (scalaVersion.startsWith("3")) ScalaParameters(Constants.defaultScala213Version) else ScalaParameters(scalaVersion) val tools = value( Artifacts.artifacts( externalRulesDeps, extraRepositories, Some(scalaParameters), logger, cache.withMessage(s"Downloading scalafix.deps") ) ) ScalafixArtifacts(scalafix.map(_._2), tools.map(_._2)) } private def fetchOrLoadScalafixProperties( extraRepositories: Seq[Repository], logger: Logger, cache: FileCache[Task] ): Either[BuildException, Properties] = either { val cacheDir = Directories.directories.cacheDir / "scalafix-props-cache" val cachePath = cacheDir / s"scalafix-interfaces-${Constants.scalafixVersion}.properties" val content = if (!os.exists(cachePath)) { val interfacesJar = value(fetchScalafixInterfaces(extraRepositories, logger, cache)) val propsData = value(readScalafixProperties(interfacesJar)) if (!os.exists(cacheDir)) os.makeDir(cacheDir) os.write(cachePath, propsData) propsData } else os.read(cachePath) val props = new Properties() val stream = new ByteArrayInputStream(content.getBytes()) props.load(stream) props } private def fetchScalafixInterfaces( extraRepositories: Seq[Repository], logger: Logger, cache: FileCache[Task] ): Either[BuildException, os.Path] = either { val scalafixInterfaces = dep"ch.epfl.scala:scalafix-interfaces:${Constants.scalafixVersion}" val fetchResult = value( Artifacts.artifacts( List(scalafixInterfaces).map(Positioned.none), extraRepositories, None, logger, cache.withMessage(s"Downloading scalafix-interfaces ${scalafixInterfaces.version}") ) ) val expectedJarName = s"scalafix-interfaces-${Constants.scalafixVersion}.jar" val interfacesJar = fetchResult.collectFirst { case (_, path) if path.last == expectedJarName => path } value( interfacesJar.toRight(new BuildException("Failed to found scalafix-interfaces jar") {}) ) } private def readScalafixProperties(jar: os.Path): Either[BuildException, String] = try { import scala.jdk.CollectionConverters.* val zipFile = ZipFile.builder().setPath(jar.toNIO).get() val entry = zipFile.getEntries().asScala.find(entry => entry.getName == "scalafix-interfaces.properties" ) val out = entry.toRight(new ScalafixPropertiesError(path = jar)) .map { entry => val stream = zipFile.getInputStream(entry) val bytes = stream.readAllBytes() new String(bytes) } zipFile.close() out } catch { case e: IOException => Left(new ScalafixPropertiesError(path = jar, cause = Some(e))) } private def scalafixPropsKey(scalaVersion: String): Either[BuildException, String] = { val regex = "(\\d)\\.(\\d+).+".r scalaVersion match { case regex("2", "12") => Right("scala212") case regex("2", "13") => Right("scala213") case regex("3", x) if x.toInt <= 3 => Right("scala3LTS") case regex("3", _) => Right("scala3Next") case _ => Left(new BuildException(s"Scalafix is not supported for Scala version: $scalaVersion") {}) } } } ================================================ FILE: modules/build/src/main/scala/scala/build/ScopedSources.scala ================================================ package scala.build import java.nio.charset.StandardCharsets import scala.build.EitherCps.{either, value} import scala.build.errors.BuildException import scala.build.info.{BuildInfo, ScopedBuildInfo} import scala.build.internal.AppCodeWrapper import scala.build.internal.util.WarningMessages import scala.build.options.{BuildOptions, HasScope, Scope} import scala.build.preprocessing.ScriptPreprocessor /** Information gathered from preprocessing command inputs - sources (including unwrapped scripts) * and build options from using directives. Only scope requirements remain in this object after * resolving them in [[CrossSources.scopedSources]] * * @param paths * paths and relative paths to sources on disk with the scope they belong to * @param inMemory * in memory sources (e.g. snippets) with the scope they belong to * @param defaultMainClass * @param resourceDirs * @param buildOptions * build options sources with the scope they belong to * @param unwrappedScripts * in memory script sources with the scope they belong to, their code must be wrapped before * compiling */ final case class ScopedSources( paths: Seq[HasScope[(os.Path, os.RelPath)]], inMemory: Seq[HasScope[Sources.InMemory]], defaultMainElemPath: Option[os.Path], resourceDirs: Seq[HasScope[os.Path]], buildOptions: Seq[HasScope[BuildOptions]], unwrappedScripts: Seq[HasScope[Sources.UnwrappedScript]] ) { def buildOptionsFor(scope: Scope): Seq[BuildOptions] = scope match { case Scope.Test => buildOptions.flatMap(_.valueFor(Scope.Test).toSeq) ++ buildOptions.flatMap(_.valueFor(Scope.Main).toSeq) case _ => buildOptions.flatMap(_.valueFor(scope).toSeq) } /** Resolve scope requirements and create a Sources instance * @param scope * scope to be resolved * @param baseOptions * options that have already been collected for this build, they should consist of: * - options from the console * - options from using directives from the sources * - options from resolved using directives that had Scala version and platform requirements * that fit the current build * @return * [[Sources]] instance that belong to specified scope */ def sources( scope: Scope, baseOptions: BuildOptions, workspace: os.Path, logger: Logger ): Either[BuildException, Sources] = either { val combinedOptions = combinedBuildOptions(scope, baseOptions) val codeWrapper = ScriptPreprocessor.getScriptWrapper(combinedOptions, logger) val wrappedScripts = unwrappedScripts .flatMap(_.valueFor(scope).toSeq) .map(_.wrap(codeWrapper)) codeWrapper match { case _: AppCodeWrapper if wrappedScripts.size > 1 => wrappedScripts.find(_.originalPath.exists(_._1.toString == "main.sc")) .foreach(_ => logger.diagnostic(WarningMessages.mainScriptNameClashesWithAppWrapper)) case _ => () } val defaultMainClass = defaultMainElemPath.flatMap { mainElemPath => wrappedScripts.collectFirst { case Sources.InMemory(Right((_, path)), _, _, Some(wrapperParams)) if mainElemPath == path => wrapperParams.mainClass } } val needsBuildInfo = combinedOptions.sourceGeneratorOptions.useBuildInfo.getOrElse(false) val maybeBuildInfoSource = if (needsBuildInfo && scope == Scope.Main) Seq( Sources.InMemory( Left("build-info"), os.rel / "BuildInfo.scala", value(buildInfo(combinedOptions, workspace)).generateContents().getBytes( StandardCharsets.UTF_8 ), None ) ) else Nil Sources( paths.flatMap(_.valueFor(scope).toSeq), inMemory.flatMap(_.valueFor(scope).toSeq) ++ wrappedScripts ++ maybeBuildInfoSource, defaultMainClass, resourceDirs.flatMap(_.valueFor(scope).toSeq), combinedOptions ) } /** Combine build options that had no requirements (console and using directives) or their * requirements have been resolved (e.g. target using directives) with build options that require * the specified scope * * @param scope * scope to be resolved * @param baseOptions * options that have already been collected for this build (had no requirements or they have * been resolved) * @return * Combined BuildOptions, baseOptions' values take precedence */ def combinedBuildOptions(scope: Scope, baseOptions: BuildOptions): BuildOptions = buildOptionsFor(scope) .foldRight(baseOptions)(_.orElse(_)) def buildInfo(baseOptions: BuildOptions, workspace: os.Path): Either[BuildException, BuildInfo] = either { def getScopedBuildInfo(scope: Scope): ScopedBuildInfo = val combinedOptions = combinedBuildOptions(scope, baseOptions) val sourcePaths = paths.flatMap(_.valueFor(scope).toSeq).map(_._1.toString) val inMemoryPaths = (inMemory.flatMap(_.valueFor(scope).toSeq).flatMap(_.originalPath.toOption) ++ unwrappedScripts.flatMap(_.valueFor(scope).toSeq).flatMap(_.originalPath.toOption)) .map(_._2.toString) ScopedBuildInfo(combinedOptions, sourcePaths ++ inMemoryPaths) val baseBuildInfo = value(BuildInfo(combinedBuildOptions(Scope.Main, baseOptions), workspace)) val mainBuildInfo = getScopedBuildInfo(Scope.Main) val testBuildInfo = getScopedBuildInfo(Scope.Test) baseBuildInfo .withScope(Scope.Main.name, mainBuildInfo) .withScope(Scope.Test.name, testBuildInfo) } } ================================================ FILE: modules/build/src/main/scala/scala/build/Sources.scala ================================================ package scala.build import coursier.cache.ArchiveCache import coursier.util.Task import java.nio.charset.StandardCharsets import scala.build.input.Inputs import scala.build.internal.{CodeWrapper, WrapperParams} import scala.build.options.{BuildOptions, Scope} import scala.build.preprocessing.* final case class Sources( paths: Seq[(os.Path, os.RelPath)], inMemory: Seq[Sources.InMemory], defaultMainClass: Option[String], resourceDirs: Seq[os.Path], buildOptions: BuildOptions ) { def withExtraSources(other: Sources): Sources = copy( paths = paths ++ other.paths, inMemory = inMemory ++ other.inMemory, resourceDirs = resourceDirs ++ other.resourceDirs ) def withVirtualDir(inputs: Inputs, scope: Scope, options: BuildOptions): Sources = { val srcRootPath = inputs.generatedSrcRoot(scope) val resourceDirs0 = options.classPathOptions.resourcesVirtualDir.map { path => srcRootPath / path } copy( resourceDirs = resourceDirs ++ resourceDirs0 ) } /** Write all in-memory sources to disk. * * @param generatedSrcRoot * the root directory where the sources should be written */ def generateSources(generatedSrcRoot: os.Path): Seq[GeneratedSource] = { val generated = for (inMemSource <- inMemory) yield { os.write.over( generatedSrcRoot / inMemSource.generatedRelPath, inMemSource.content, createFolders = true ) ( inMemSource.originalPath.map(_._2), inMemSource.generatedRelPath, inMemSource.wrapperParamsOpt ) } val generatedSet = generated.map(_._2).toSet if (os.isDir(generatedSrcRoot)) os.walk(generatedSrcRoot) .filter(os.isFile(_)) .filter(p => !generatedSet(p.relativeTo(generatedSrcRoot))) .foreach(os.remove(_)) generated.map { case (reportingPath, path, wrapperParamsOpt) => GeneratedSource(generatedSrcRoot / path, reportingPath, wrapperParamsOpt) } } lazy val hasJava = (paths.iterator.map(_._1.last) ++ inMemory.iterator.map(_.generatedRelPath.last)) .exists(_.endsWith(".java")) lazy val hasScala = (paths.iterator.map(_._1.last) ++ inMemory.iterator.map(_.generatedRelPath.last)) .exists(_.endsWith(".scala")) } object Sources { final case class InMemory( originalPath: Either[String, (os.SubPath, os.Path)], generatedRelPath: os.RelPath, content: Array[Byte], wrapperParamsOpt: Option[WrapperParams] ) final case class UnwrappedScript( originalPath: Either[String, (os.SubPath, os.Path)], generatedRelPath: os.RelPath, wrapScriptFun: CodeWrapper => (String, WrapperParams) ) { def wrap(wrapper: CodeWrapper): InMemory = { val (content, wrapperParams) = wrapScriptFun(wrapper) InMemory( originalPath, generatedRelPath, content.getBytes(StandardCharsets.UTF_8), Some(wrapperParams) ) } } /** The default preprocessor list. * * @param archiveCache * used from native launchers by the Java preprocessor, to download a java-class-name binary, * used to infer the class name of unnamed Java sources (like stdin) * @param javaClassNameVersionOpt * if using a java-class-name binary, the version we should download. If empty, the default * version is downloaded. * @return */ def defaultPreprocessors( archiveCache: ArchiveCache[Task], javaClassNameVersionOpt: Option[String], javaCommand: () => String ): Seq[Preprocessor] = Seq( ScriptPreprocessor, MarkdownPreprocessor, JavaPreprocessor(archiveCache, javaClassNameVersionOpt, javaCommand), ScalaPreprocessor, DataPreprocessor, JarPreprocessor ) } ================================================ FILE: modules/build/src/main/scala/scala/build/bsp/BloopSession.scala ================================================ package scala.build.bsp import com.swoval.files.PathWatchers import java.util.concurrent.atomic.AtomicReference import scala.build.Build import scala.build.compiler.BloopCompiler import scala.build.input.{Inputs, OnDisk, SingleFile, Virtual} final class BloopSession( val inputs: Inputs, val inputsHash: String, val remoteServer: BloopCompiler, val bspServer: BspServer, val watcher: Build.Watcher ) { def resetDiagnostics(localClient: BspClient): Unit = for (targetId <- bspServer.targetIds) inputs.flattened().foreach { case f: SingleFile => localClient.resetDiagnostics(f.path, targetId) case _: Virtual => } def dispose(): Unit = { watcher.dispose() remoteServer.shutdown() } def registerWatchInputs(): Unit = inputs.elements.foreach { case elem: OnDisk => val eventFilter: PathWatchers.Event => Boolean = { event => val newOrDeletedFile = event.getKind == PathWatchers.Event.Kind.Create || event.getKind == PathWatchers.Event.Kind.Delete lazy val p = os.Path(event.getTypedPath.getPath.toAbsolutePath) lazy val relPath = p.relativeTo(elem.path) lazy val isHidden = relPath.segments.exists(_.startsWith(".")) def isScalaFile = relPath.last.endsWith(".sc") || relPath.last.endsWith(".scala") def isJavaFile = relPath.last.endsWith(".java") newOrDeletedFile && !isHidden && (isScalaFile || isJavaFile) } val watcher0 = watcher.newWatcher() watcher0.register(elem.path.toNIO, Int.MaxValue) watcher0.addObserver { Build.onChangeBufferedObserver { event => if (eventFilter(event)) watcher.schedule() } } case _ => } } object BloopSession { def apply( inputs: Inputs, remoteServer: BloopCompiler, bspServer: BspServer, watcher: Build.Watcher ): BloopSession = new BloopSession(inputs, inputs.sourceHash(), remoteServer, bspServer, watcher) final class Reference { private val ref = new AtomicReference[BloopSession](null) def get(): BloopSession = { val session = ref.get() if (session == null) sys.error("BSP server not initialized yet") session } def getAndNullify(): Option[BloopSession] = Option(ref.getAndSet(null)) def update(former: BloopSession, newer: BloopSession, ifError: String): Unit = if (!ref.compareAndSet(former, newer)) sys.error(ifError) } } ================================================ FILE: modules/build/src/main/scala/scala/build/bsp/Bsp.scala ================================================ package scala.build.bsp import java.io.{InputStream, OutputStream} import scala.build.errors.BuildException import scala.build.input.{Inputs, ScalaCliInvokeData} import scala.concurrent.Future trait Bsp { def run(initialInputs: Inputs, initialBspOptions: BspReloadableOptions): Future[Unit] def shutdown(): Unit } object Bsp { def create( argsToInputs: Seq[String] => Either[BuildException, Inputs], bspReloadableOptionsReference: BspReloadableOptions.Reference, threads: BspThreads, in: InputStream, out: OutputStream, actionableDiagnostics: Option[Boolean] )(using ScalaCliInvokeData): Bsp = new BspImpl( argsToInputs, bspReloadableOptionsReference, threads, in, out, actionableDiagnostics ) } ================================================ FILE: modules/build/src/main/scala/scala/build/bsp/BspClient.scala ================================================ package scala.build.bsp import ch.epfl.scala.bsp4j as b import ch.epfl.scala.bsp4j.{ScalaAction, ScalaDiagnostic, ScalaTextEdit, ScalaWorkspaceEdit} import com.google.gson.{Gson, JsonElement} import java.lang.Boolean as JBoolean import java.net.URI import java.nio.file.Paths import java.util.concurrent.ConcurrentHashMap import scala.build.Position.File import scala.build.errors.{BuildException, CompositeBuildException, Diagnostic} import scala.build.internal.util.WarningMessages import scala.build.postprocessing.LineConversion.scalaLineToScLine import scala.build.{BloopBuildClient, GeneratedSource, Logger} import scala.jdk.CollectionConverters.* class BspClient( @volatile var logger: Logger, var forwardToOpt: Option[b.BuildClient] = None ) extends b.BuildClient with BuildClientForwardStubs with BloopBuildClient with HasGeneratedSourcesImpl { private def updatedPublishDiagnosticsParams( params: b.PublishDiagnosticsParams, genSource: GeneratedSource ): b.PublishDiagnosticsParams = { val updatedUri = genSource.reportingPath.fold( _ => params.getTextDocument.getUri, _.toNIO.toUri.toASCIIString ) val updatedDiagnostics = if (genSource.wrapperParamsOpt.isEmpty) params.getDiagnostics else val updateLine = scalaLine => scalaLineToScLine(scalaLine, genSource.wrapperParamsOpt) params.getDiagnostics.asScala.toSeq .map { diag => val updatedDiagOpt = for { startLine <- updateLine(diag.getRange.getStart.getLine) endLine <- updateLine(diag.getRange.getEnd.getLine) } yield { val diag0 = diag.duplicate() diag0.getRange.getStart.setLine(startLine) diag0.getRange.getEnd.setLine(endLine) val scalaDiagnostic = new Gson().fromJson[b.ScalaDiagnostic]( diag0.getData().asInstanceOf[JsonElement], classOf[b.ScalaDiagnostic] ) scalaDiagnostic.getActions().asScala.foreach { action => for { change <- action.getEdit().getChanges().asScala startLine <- updateLine(change.getRange.getStart.getLine) endLine <- updateLine(change.getRange.getEnd.getLine) } yield { change.getRange().getStart.setLine(startLine) change.getRange().getEnd.setLine(endLine) } } diag0.setData(scalaDiagnostic) if ( diag0.getMessage.contains( "cannot be a main method since it cannot be accessed statically" ) ) diag0.setMessage( WarningMessages.mainAnnotationNotSupported( /* annotationIgnored */ false) ) diag0 } updatedDiagOpt.getOrElse(diag) } .asJava val updatedTextDoc = new b.TextDocumentIdentifier(updatedUri) val updatedParams = new b.PublishDiagnosticsParams( updatedTextDoc, params.getBuildTarget, updatedDiagnostics, params.getReset ) updatedParams.setOriginId(params.getOriginId) updatedParams } override def onBuildPublishDiagnostics(params: b.PublishDiagnosticsParams): Unit = { val path = os.Path(Paths.get(new URI(params.getTextDocument.getUri))) buildExceptionDiagnosticsDocs.remove((path, params.getBuildTarget)) actualBuildPublishDiagnostics(params) } private def actualBuildPublishDiagnostics(params: b.PublishDiagnosticsParams): Unit = { val updatedParamsOpt = targetScopeOpt(params.getBuildTarget).flatMap { scope => generatedSources.getOrElse(scope, HasGeneratedSources.GeneratedSources(Nil)) .uriMap .get(params.getTextDocument.getUri) .map { genSource => updatedPublishDiagnosticsParams(params, genSource) } } updatedParamsOpt match { case None => super.onBuildPublishDiagnostics(params) case Some(updatedParams) => super.onBuildPublishDiagnostics(updatedParams) } } def setProjectParams(newParams: Seq[String]): Unit = {} def diagnostics: Option[Seq[(Either[String, os.Path], b.Diagnostic)]] = None def clear(): Unit = {} private val buildExceptionDiagnosticsDocs = new ConcurrentHashMap[(os.Path, b.BuildTargetIdentifier), JBoolean] def resetDiagnostics(path: os.Path, targetId: b.BuildTargetIdentifier): Unit = { val id = new b.TextDocumentIdentifier(path.toNIO.toUri.toASCIIString) val params = new b.PublishDiagnosticsParams( id, targetId, List.empty[b.Diagnostic].asJava, true ) actualBuildPublishDiagnostics(params) } def resetBuildExceptionDiagnostics(targetId: b.BuildTargetIdentifier): Unit = for { (key @ (path, elemTargetId), _) <- buildExceptionDiagnosticsDocs.asScala.toVector if elemTargetId == targetId } { val removedValue = buildExceptionDiagnosticsDocs.remove(key) if (removedValue != null) { val id = new b.TextDocumentIdentifier(path.toNIO.toUri.toASCIIString) val params = new b.PublishDiagnosticsParams( id, targetId, List.empty[b.Diagnostic].asJava, true ) actualBuildPublishDiagnostics(params) } } def reportBuildException( targetIdOpt: Option[b.BuildTargetIdentifier], ex: BuildException, reset: Boolean = true ): Unit = targetIdOpt match { case None => logger.debug(s"Not reporting $ex to users (no build target id)") case Some(targetId) => val touchedFiles = (ex match { case c: CompositeBuildException => reportDiagnosticsForFiles(targetId, c.exceptions, reset = reset) case _ => reportDiagnosticsForFiles(targetId, Seq(ex), reset = reset) }).toSet // Small chance of us wiping some Bloop diagnostics, if these happen // between the call to remove and the call to actualBuildPublishDiagnostics. for { (key @ (path, elemTargetId), _) <- buildExceptionDiagnosticsDocs.asScala.toVector if elemTargetId == targetId && !touchedFiles(path) } { val removedValue = buildExceptionDiagnosticsDocs.remove(key) if (removedValue != null) { val id = new b.TextDocumentIdentifier(path.toNIO.toUri.toASCIIString) val params = new b.PublishDiagnosticsParams( id, targetId, List.empty[b.Diagnostic].asJava, true ) actualBuildPublishDiagnostics(params) } } } def reportDiagnosticsForFiles( targetId: b.BuildTargetIdentifier, diags: Seq[Diagnostic], reset: Boolean = true ): Seq[os.Path] = if reset then // send diagnostic with reset only once for every file path diags.flatMap { diag => diag.positions.map { position => Diagnostic(diag.message, diag.severity, Seq(position), diag.textEdit) } } .groupBy(_.positions.headOption match case Some(File(Right(path), _, _, _)) => Some(path) case _ => None) .filter(_._1.isDefined) .values .toSeq .flatMap { case head :: tail => reportDiagnosticForFiles(targetId, reset = reset)(head) ++ tail.flatMap(reportDiagnosticForFiles(targetId)) case _ => Nil } else diags.flatMap(reportDiagnosticForFiles(targetId)) private def reportDiagnosticForFiles( targetId: b.BuildTargetIdentifier, reset: Boolean = false )(diag: Diagnostic): Seq[os.Path] = diag.positions.flatMap { case File(Right(path), (startLine, startC), (endL, endC), _) => val id = new b.TextDocumentIdentifier(path.toNIO.toUri.toASCIIString) val startPos = new b.Position(startLine, startC) val endPos = new b.Position(endL, endC) val range = new b.Range(startPos, endPos) val bDiag = new b.Diagnostic(range, diag.message) diag.textEdit.foreach { textEdit => val bScalaTextEdit = new ScalaTextEdit(range, textEdit.newText) val bScalaWorkspaceEdit = new ScalaWorkspaceEdit(List(bScalaTextEdit).asJava) val bAction = new ScalaAction(textEdit.title) bAction.setEdit(bScalaWorkspaceEdit) val bScalaDiagnostic = new ScalaDiagnostic bScalaDiagnostic.setActions(List(bAction).asJava) bDiag.setDataKind("scala") bDiag.setData(new Gson().toJsonTree(bScalaDiagnostic)) } bDiag.setSeverity(diag.severity.toBsp4j) bDiag.setSource("scala-cli") val params = new b.PublishDiagnosticsParams( id, targetId, List(bDiag).asJava, reset ) buildExceptionDiagnosticsDocs.put((path, targetId), JBoolean.TRUE) actualBuildPublishDiagnostics(params) Seq(path) case _ => Nil } } ================================================ FILE: modules/build/src/main/scala/scala/build/bsp/BspImpl.scala ================================================ package scala.build.bsp import bloop.rifle.BloopServer import ch.epfl.scala.bsp4j as b import com.github.plokhotnyuk.jsoniter_scala.core.{JsonReaderException, readFromArray} import dependency.ScalaParameters import org.eclipse.lsp4j.jsonrpc import org.eclipse.lsp4j.jsonrpc.messages.ResponseError import java.io.{InputStream, OutputStream} import java.util.UUID import java.util.concurrent.{CompletableFuture, Executor} import scala.build.* import scala.build.EitherCps.{either, value} import scala.build.compiler.BloopCompiler import scala.build.errors.{ BuildException, CompositeBuildException, Diagnostic, ParsingInputsException } import scala.build.input.{Inputs, ScalaCliInvokeData} import scala.build.internal.Constants import scala.build.options.{BuildOptions, Scope} import scala.collection.mutable.ListBuffer import scala.compiletime.uninitialized import scala.concurrent.duration.DurationInt import scala.concurrent.{ExecutionContext, Future, Promise} import scala.jdk.CollectionConverters.* import scala.util.{Failure, Success} /** The implementation for [[Bsp]] command. * * @param argsToInputs * a function transforming terminal args to [[Inputs]] * @param bspReloadableOptionsReference * reference to the current instance of [[BspReloadableOptions]] * @param threads * BSP threads * @param in * the input stream of bytes * @param out * the output stream of bytes */ final class BspImpl( argsToInputs: Seq[String] => Either[BuildException, Inputs], bspReloadableOptionsReference: BspReloadableOptions.Reference, threads: BspThreads, in: InputStream, out: OutputStream, actionableDiagnostics: Option[Boolean] )(using ScalaCliInvokeData) extends Bsp { import BspImpl.{PreBuildData, PreBuildProject, buildTargetIdToEvent, responseError} private val shownGlobalMessages = new java.util.concurrent.ConcurrentHashMap[String, Unit]() private var actualLocalClient: BspClient = uninitialized private var localClient: b.BuildClient & BloopBuildClient = uninitialized private val bloopSession = new BloopSession.Reference /** Sends the buildTarget/didChange BSP notification to the BSP client, indicating that the build * targets defined in the current session have changed. * * @param currentBloopSession * the current Bloop session */ private def notifyBuildChange(currentBloopSession: BloopSession): Unit = { val events = for (targetId <- currentBloopSession.bspServer.targetIds) yield { val event = new b.BuildTargetEvent(targetId) event.setKind(b.BuildTargetEventKind.CHANGED) event } val params = new b.DidChangeBuildTarget(events.asJava) actualLocalClient.onBuildTargetDidChange(params) } /** Initial setup for the Bloop project. * * @param currentBloopSession * the current Bloop session * @param reloadableOptions * options which may be reloaded on a bsp workspace/reload request * @param maybeRecoverOnError * a function handling [[BuildException]] instances based on [[Scope]], possibly recovering * them; returns None on recovery, Some(e: BuildException) otherwise */ private def prepareBuild( currentBloopSession: BloopSession, reloadableOptions: BspReloadableOptions, maybeRecoverOnError: Scope => BuildException => Option[BuildException] = _ => e => Some(e) ): Either[(BuildException, Scope), PreBuildProject] = either[(BuildException, Scope)] { val logger = reloadableOptions.logger val buildOptions = reloadableOptions.buildOptions val verbosity = reloadableOptions.verbosity logger.log("Preparing build") val persistentLogger = new PersistentDiagnosticLogger(logger) val bspServer = currentBloopSession.bspServer val inputs = currentBloopSession.inputs // allInputs contains elements from using directives val (crossSources, allInputs) = value { CrossSources.forInputs( inputs = inputs, preprocessors = Sources.defaultPreprocessors( buildOptions.archiveCache, buildOptions.internal.javaClassNameVersionOpt, () => buildOptions.javaHome().value.javaCommand ), logger = persistentLogger, suppressWarningOptions = buildOptions.suppressWarningOptions, exclude = buildOptions.internal.exclude, maybeRecoverOnError = maybeRecoverOnError(Scope.Main), download = buildOptions.downloader ).left.map((_, Scope.Main)) } val sharedOptions = crossSources.sharedOptions(buildOptions) if (verbosity >= 3) pprint.err.log(crossSources) val scopedSources = value(crossSources.scopedSources(buildOptions).left.map((_, Scope.Main))) if (verbosity >= 3) pprint.err.log(scopedSources) val sourcesMain = value { scopedSources.sources(Scope.Main, sharedOptions, allInputs.workspace, persistentLogger) .left.map((_, Scope.Main)) } val sourcesTest = value { scopedSources.sources(Scope.Test, sharedOptions, allInputs.workspace, persistentLogger) .left.map((_, Scope.Test)) } if (verbosity >= 3) pprint.err.log(sourcesMain) val options0Main = sourcesMain.buildOptions val options0Test = sourcesTest.buildOptions.orElse(options0Main) val generatedSourcesMain = sourcesMain.generateSources(allInputs.generatedSrcRoot(Scope.Main)) val generatedSourcesTest = sourcesTest.generateSources(allInputs.generatedSrcRoot(Scope.Test)) bspServer.setExtraDependencySources(options0Main.classPathOptions.extraSourceJars) bspServer.setExtraTestDependencySources(options0Test.classPathOptions.extraSourceJars) bspServer.setGeneratedSources(Scope.Main, generatedSourcesMain) bspServer.setGeneratedSources(Scope.Test, generatedSourcesTest) val (classesDir0Main, scalaParamsMain, artifactsMain, projectMain, buildChangedMain) = value { val res = Build.prepareBuild( inputs = allInputs, sources = sourcesMain, generatedSources = generatedSourcesMain, options = options0Main, scope = Scope.Main, compiler = currentBloopSession.remoteServer, logger = persistentLogger, buildClient = localClient, maybeRecoverOnError = maybeRecoverOnError(Scope.Main) ) res.left.map((_, Scope.Main)) } val (classesDir0Test, scalaParamsTest, artifactsTest, projectTest, buildChangedTest) = value { val res = Build.prepareBuild( inputs = allInputs, sources = sourcesTest, generatedSources = generatedSourcesTest, options = options0Test, scope = Scope.Test, compiler = currentBloopSession.remoteServer, logger = persistentLogger, buildClient = localClient, maybeRecoverOnError = maybeRecoverOnError(Scope.Test) ) res.left.map((_, Scope.Test)) } localClient.setGeneratedSources(Scope.Main, generatedSourcesMain) localClient.setGeneratedSources(Scope.Test, generatedSourcesTest) val mainScope = PreBuildData( sourcesMain, options0Main, classesDir0Main, scalaParamsMain, artifactsMain, projectMain, generatedSourcesMain, buildChangedMain ) val testScope = PreBuildData( sourcesTest, options0Test, classesDir0Test, scalaParamsTest, artifactsTest, projectTest, generatedSourcesTest, buildChangedTest ) if (actionableDiagnostics.getOrElse(true)) { val projectOptions = options0Test.orElse(options0Main) projectOptions.logActionableDiagnostics(persistentLogger) } PreBuildProject(mainScope, testScope, persistentLogger.diagnostics) } private def buildE( currentBloopSession: BloopSession, notifyChanges: Boolean, reloadableOptions: BspReloadableOptions ): Either[(BuildException, Scope), Unit] = { def doBuildOnce(data: PreBuildData, scope: Scope): Either[(BuildException, Scope), Build] = Build.buildOnce( currentBloopSession.inputs, data.sources, data.generatedSources, data.buildOptions, scope, reloadableOptions.logger, actualLocalClient, currentBloopSession.remoteServer, partialOpt = None ).left.map(_ -> scope) either[(BuildException, Scope)] { val preBuild = value(prepareBuild(currentBloopSession, reloadableOptions)) if (notifyChanges && (preBuild.mainScope.buildChanged || preBuild.testScope.buildChanged)) notifyBuildChange(currentBloopSession) value(doBuildOnce(preBuild.mainScope, Scope.Main)) value(doBuildOnce(preBuild.testScope, Scope.Test)) () } } private def build( currentBloopSession: BloopSession, client: BspClient, notifyChanges: Boolean, reloadableOptions: BspReloadableOptions ): Unit = buildE(currentBloopSession, notifyChanges, reloadableOptions) match { case Left((ex, scope)) => client.reportBuildException( currentBloopSession.bspServer.targetScopeIdOpt(scope), ex ) reloadableOptions.logger.debug(s"Caught $ex during BSP build, ignoring it") case Right(()) => for (targetId <- currentBloopSession.bspServer.targetIds) client.resetBuildExceptionDiagnostics(targetId) } private def showGlobalWarningOnce(msg: String): Unit = shownGlobalMessages.computeIfAbsent( msg, _ => { val params = new b.ShowMessageParams(b.MessageType.WARNING, msg) actualLocalClient.onBuildShowMessage(params) } ) /** Compilation logic, to be called on a buildTarget/compile BSP request. * * @param currentBloopSession * the current Bloop session * @param executor * executor * @param reloadableOptions * options which may be reloaded on a bsp workspace/reload request * @param doCompile * (self-)reference to calling the compilation logic * @return * a future of [[b.CompileResult]] */ private def compile( currentBloopSession: BloopSession, executor: Executor, reloadableOptions: BspReloadableOptions, doCompile: () => CompletableFuture[b.CompileResult] ): CompletableFuture[b.CompileResult] = { val preBuild = CompletableFuture.supplyAsync( () => prepareBuild(currentBloopSession, reloadableOptions) match { case Right(preBuild) => if (preBuild.mainScope.buildChanged || preBuild.testScope.buildChanged) notifyBuildChange(currentBloopSession) Right(preBuild) case Left((ex, scope)) => Left((ex, scope)) }, executor ) preBuild.thenCompose { case Left((ex, scope)) => val taskId = new b.TaskId(UUID.randomUUID().toString) for targetId <- currentBloopSession.bspServer.targetScopeIdOpt(scope) do { val target = targetId.getUri match { case s"$_?id=$targetId" => targetId case targetIdUri => targetIdUri } val taskStartParams = new b.TaskStartParams(taskId) taskStartParams.setEventTime(System.currentTimeMillis()) taskStartParams.setMessage(s"Preprocessing '$target'") taskStartParams.setDataKind(b.TaskStartDataKind.COMPILE_TASK) taskStartParams.setData(new b.CompileTask(targetId)) actualLocalClient.onBuildTaskStart(taskStartParams) actualLocalClient.reportBuildException( Some(targetId), ex ) val taskFinishParams = new b.TaskFinishParams(taskId, b.StatusCode.ERROR) taskFinishParams.setEventTime(System.currentTimeMillis()) taskFinishParams.setMessage(s"Preprocessed '$target'") taskFinishParams.setDataKind(b.TaskFinishDataKind.COMPILE_REPORT) val errorSize = ex match { case c: CompositeBuildException => c.exceptions.size case _ => 1 } taskFinishParams.setData(new b.CompileReport(targetId, errorSize, 0)) actualLocalClient.onBuildTaskFinish(taskFinishParams) } CompletableFuture.completedFuture( new b.CompileResult(b.StatusCode.ERROR) ) case Right(params) => for (targetId <- currentBloopSession.bspServer.targetIds) actualLocalClient.resetBuildExceptionDiagnostics(targetId) val targetId = currentBloopSession.bspServer.targetIds.head actualLocalClient.reportDiagnosticsForFiles(targetId, params.diagnostics, reset = false) doCompile().thenCompose { res => def doPostProcess(data: PreBuildData, scope: Scope): Unit = for (sv <- data.project.scalaCompiler.map(_.scalaVersion)) Build.postProcess( data.generatedSources, currentBloopSession.inputs.generatedSrcRoot(scope), data.classesDir, reloadableOptions.logger, currentBloopSession.inputs.workspace, updateSemanticDbs = true, scalaVersion = sv, buildOptions = data.buildOptions ).left.foreach(_.foreach(showGlobalWarningOnce)) if (res.getStatusCode == b.StatusCode.OK) CompletableFuture.supplyAsync( () => { doPostProcess(params.mainScope, Scope.Main) doPostProcess(params.testScope, Scope.Test) res }, executor ) else CompletableFuture.completedFuture(res) } } } /** Returns a reference to the [[BspClient]], respecting the given verbosity * @param verbosity * verbosity to be passed to the resulting [[BspImpl.LoggingBspClient]] * @return * BSP client */ private def getLocalClient(verbosity: Int): b.BuildClient & BloopBuildClient = if (verbosity >= 3) new BspImpl.LoggingBspClient(actualLocalClient) else actualLocalClient /** Creates a fresh Bloop session * @param inputs * all the inputs to be included in the session's context * @param reloadableOptions * options which may be reloaded on a bsp workspace/reload request * @param presetIntelliJ * a flag marking if this is in context of a BSP connection with IntelliJ (allowing to pass * this setting from a past session) * @return * a new [[BloopSession]] */ private def newBloopSession( inputs: Inputs, reloadableOptions: BspReloadableOptions, presetIntelliJ: Boolean = false ): BloopSession = { val logger = reloadableOptions.logger val buildOptions = reloadableOptions.buildOptions val createBloopServer = () => BloopServer.buildServer( reloadableOptions.bloopRifleConfig, "scala-cli", Constants.version, (inputs.workspace / Constants.workspaceDirName).toNIO, Build.classesRootDir(inputs.workspace, inputs.projectName).toNIO, localClient, threads.buildThreads.bloop, logger.bloopRifleLogger ) val remoteServer = new BloopCompiler( createBloopServer, 20.seconds, strictBloopJsonCheck = buildOptions.internal.strictBloopJsonCheckOrDefault ) lazy val bspServer = new BspServer( remoteServer.bloopServer.server, doCompile => compile(bloopSession0, threads.prepareBuildExecutor, reloadableOptions, doCompile), logger, presetIntelliJ ) lazy val watcher = new Build.Watcher( ListBuffer(), threads.buildThreads.fileWatcher, build(bloopSession0, actualLocalClient, notifyChanges = true, reloadableOptions), () ) lazy val bloopSession0: BloopSession = BloopSession(inputs, remoteServer, bspServer, watcher) bloopSession0.registerWatchInputs() bspServer.newInputs(inputs) bloopSession0 } /** The logic for the actual running of the `bsp` command, initializing the BSP connection. * @param initialInputs * the initial input sources passed upon initializing the BSP connection (which are subject to * change on subsequent workspace/reload requests) */ override def run(initialInputs: Inputs, initialBspOptions: BspReloadableOptions): Future[Unit] = { val logger = initialBspOptions.logger val verbosity = initialBspOptions.verbosity actualLocalClient = new BspClient(logger) localClient = getLocalClient(verbosity) val currentBloopSession = newBloopSession(initialInputs, initialBspOptions) bloopSession.update(null, currentBloopSession, "BSP server already initialized") val actualLocalServer: b.BuildServer & b.ScalaBuildServer & b.JavaBuildServer & b.JvmBuildServer & ScalaScriptBuildServer & HasGeneratedSources = new BuildServerProxy( () => bloopSession.get().bspServer, () => onReload() ) val localServer: b.BuildServer & b.ScalaBuildServer & b.JavaBuildServer & b.JvmBuildServer & ScalaScriptBuildServer = if verbosity >= 3 then new LoggingBuildServerAll(actualLocalServer) else actualLocalServer val launcher = new jsonrpc.Launcher.Builder[b.BuildClient]() .setExecutorService(threads.buildThreads.bloop.jsonrpc) // FIXME No .setInput(in) .setOutput(out) .setRemoteInterface(classOf[b.BuildClient]) .setLocalService(localServer) .create() val remoteClient = launcher.getRemoteProxy actualLocalClient.forwardToOpt = Some(remoteClient) actualLocalClient.newInputs(initialInputs) currentBloopSession.resetDiagnostics(actualLocalClient) val recoverOnError: Scope => BuildException => Option[BuildException] = scope => e => { actualLocalClient.reportBuildException(actualLocalServer.targetScopeIdOpt(scope), e) logger.log(e) None } prepareBuild( currentBloopSession, initialBspOptions, maybeRecoverOnError = recoverOnError ) match { case Left((ex, scope)) => recoverOnError(scope)(ex) case Right(_) => } logger.log { val hasConsole = System.console() != null if (hasConsole) "Listening to incoming JSONRPC BSP requests, press Ctrl+D to exit." else "Listening to incoming JSONRPC BSP requests." } val f = launcher.startListening() val initiateFirstBuild: Runnable = { () => try build(currentBloopSession, actualLocalClient, notifyChanges = false, initialBspOptions) catch { case t: Throwable => logger.debug(s"Caught $t during initial BSP build, ignoring it") } } threads.prepareBuildExecutor.submit(initiateFirstBuild) val es = ExecutionContext.fromExecutorService(threads.buildThreads.bloop.jsonrpc) val futures = Seq( BspImpl.naiveJavaFutureToScalaFuture(f).map(_ => ())(using es), currentBloopSession.bspServer.initiateShutdown ) Future.firstCompletedOf(futures)(using es) } /** Shuts down the current Bloop session */ override def shutdown(): Unit = for (currentBloopSession <- bloopSession.getAndNullify()) currentBloopSession.dispose() /** BSP reload logic, to be used on a workspace/reload BSP request * * @param currentBloopSession * the current Bloop session * @param previousInputs * all the input sources present in the context before the reload * @param newInputs * all the input sources to be included in the new context after the reload * @param reloadableOptions * options which may be reloaded on a bsp workspace/reload request * @return * a future containing a valid workspace/reload response */ private def reloadBsp( currentBloopSession: BloopSession, previousInputs: Inputs, newInputs: Inputs, reloadableOptions: BspReloadableOptions ): CompletableFuture[AnyRef] = { val previousTargetIds = currentBloopSession.bspServer.targetIds val wasIntelliJ = currentBloopSession.bspServer.isIntelliJ currentBloopSession.dispose() val newBloopSession0 = newBloopSession(newInputs, reloadableOptions, wasIntelliJ) bloopSession.update(currentBloopSession, newBloopSession0, "Concurrent reload of workspace") actualLocalClient.newInputs(newInputs) newBloopSession0.resetDiagnostics(actualLocalClient) prepareBuild(newBloopSession0, reloadableOptions) match { case Left((buildException, scope)) => CompletableFuture.completedFuture( responseError( s"Can't reload workspace, build failed for scope ${scope.name}: ${buildException.message}" ) ) case Right(preBuildProject) => lazy val projectJavaHome = preBuildProject.mainScope.buildOptions .javaHome() .value val finalBloopSession = if ( bloopSession.get().remoteServer.jvmVersion.exists(_.value < projectJavaHome.version) ) { reloadableOptions.logger.log( s"Bloop JVM version too low, current ${bloopSession.get().remoteServer.jvmVersion.get .value} expected ${projectJavaHome.version}, restarting server" ) // RelodableOptions don't take into account buildOptions from sources val updatedReloadableOptions = reloadableOptions.copy( buildOptions = reloadableOptions.buildOptions.orElse(preBuildProject.mainScope.buildOptions), bloopRifleConfig = reloadableOptions.bloopRifleConfig.copy( javaPath = projectJavaHome.javaCommand, minimumBloopJvm = projectJavaHome.version ) ) newBloopSession0.dispose() val bloopSessionWithJvmOkay = newBloopSession(newInputs, updatedReloadableOptions, wasIntelliJ) bloopSession.update( newBloopSession0, bloopSessionWithJvmOkay, "Concurrent reload of workspace" ) bloopSessionWithJvmOkay } else newBloopSession0 if (previousInputs.projectName != preBuildProject.mainScope.project.projectName) for (client <- finalBloopSession.bspServer.clientOpt) { val newTargetIds = finalBloopSession.bspServer.targetIds val events = newTargetIds.map(buildTargetIdToEvent(_, b.BuildTargetEventKind.CREATED)) ++ previousTargetIds.map(buildTargetIdToEvent(_, b.BuildTargetEventKind.DELETED)) val didChangeBuildTargetParams = new b.DidChangeBuildTarget(events.asJava) client.onBuildTargetDidChange(didChangeBuildTargetParams) } CompletableFuture.completedFuture(new Object()) } } /** All the logic surrounding a workspace/reload (establishing the new inputs, settings and * refreshing all the relevant variables), including the actual BSP workspace reloading. * * @return * a future containing a valid workspace/reload response */ private def onReload(): CompletableFuture[AnyRef] = { val currentBloopSession = bloopSession.get() bspReloadableOptionsReference.reload() val reloadableOptions = bspReloadableOptionsReference.get val logger = reloadableOptions.logger val verbosity = reloadableOptions.verbosity actualLocalClient.logger = logger localClient = getLocalClient(verbosity) val ideInputsJsonPath = currentBloopSession.inputs.workspace / Constants.workspaceDirName / "ide-inputs.json" if (os.isFile(ideInputsJsonPath)) { val maybeResponse = either[BuildException] { val ideInputs = value { try Right(readFromArray(os.read.bytes(ideInputsJsonPath))(using IdeInputs.codec)) catch { case e: JsonReaderException => logger.debug(s"Caught $e while decoding $ideInputsJsonPath") Left(new ParsingInputsException(e.getMessage, e)) } } val newInputs = value(argsToInputs(ideInputs.args)) val newHash = newInputs.sourceHash() val previousInputs = currentBloopSession.inputs val previousHash = currentBloopSession.inputsHash if newInputs == previousInputs && newHash == previousHash then CompletableFuture.completedFuture(new Object) else reloadBsp(currentBloopSession, previousInputs, newInputs, reloadableOptions) } maybeResponse match { case Left(errorMessage) => CompletableFuture.completedFuture( responseError(s"Workspace reload failed, couldn't load sources: $errorMessage") ) case Right(r) => r } } else CompletableFuture.completedFuture( responseError( s"Workspace reload failed, inputs file missing from workspace directory: ${ideInputsJsonPath.toString()}" ) ) } } object BspImpl { private def buildTargetIdToEvent( targetId: b.BuildTargetIdentifier, eventKind: b.BuildTargetEventKind ): b.BuildTargetEvent = { val event = new b.BuildTargetEvent(targetId) event.setKind(eventKind) event } private def responseError( message: String, errorCode: Int = JsonRpcErrorCodes.InternalError ): ResponseError = new ResponseError(errorCode, message, new Object()) // from https://github.com/com-lihaoyi/Ammonite/blob/7eb58c58ec8c252dc5bd1591b041fcae01cccf90/amm/interp/src/main/scala/ammonite/interp/script/AmmoniteBuildServer.scala#L550-L565 private def naiveJavaFutureToScalaFuture[T]( f: java.util.concurrent.Future[T] ): Future[T] = { val p = Promise[T]() val t = new Thread { setDaemon(true) setName("bsp-wait-for-exit") override def run(): Unit = p.complete { try Success(f.get()) catch { case t: Throwable => Failure(t) } } } t.start() p.future } private final class LoggingBspClient(actualLocalClient: BspClient) extends LoggingBuildClient with BloopBuildClient { // in Scala 3 type of the method needs to be explicitly overridden def underlying: scala.build.bsp.BspClient = actualLocalClient def clear() = underlying.clear() def diagnostics = underlying.diagnostics def setProjectParams(newParams: Seq[String]) = underlying.setProjectParams(newParams) def setGeneratedSources(scope: Scope, newGeneratedSources: Seq[GeneratedSource]) = underlying.setGeneratedSources(scope, newGeneratedSources) } private final case class PreBuildData( sources: Sources, buildOptions: BuildOptions, classesDir: os.Path, scalaParams: Option[ScalaParameters], artifacts: Artifacts, project: Project, generatedSources: Seq[GeneratedSource], buildChanged: Boolean ) private final case class PreBuildProject( mainScope: PreBuildData, testScope: PreBuildData, diagnostics: Seq[Diagnostic] ) } ================================================ FILE: modules/build/src/main/scala/scala/build/bsp/BspReloadableOptions.scala ================================================ package scala.build.bsp import bloop.rifle.BloopRifleConfig import scala.build.Logger import scala.build.options.BuildOptions /** The options and configurations that may be picked up on a bsp workspace/reload request. * * @param buildOptions * passed options for building sources * @param bloopRifleConfig * configuration for bloop-rifle * @param logger * logger * @param verbosity * the verbosity of logs */ case class BspReloadableOptions( buildOptions: BuildOptions, bloopRifleConfig: BloopRifleConfig, logger: Logger, verbosity: Int ) object BspReloadableOptions { class Reference(getReloaded: () => BspReloadableOptions) { @volatile private var ref: BspReloadableOptions = getReloaded() def get: BspReloadableOptions = ref def reload(): Unit = ref = getReloaded() } } ================================================ FILE: modules/build/src/main/scala/scala/build/bsp/BspServer.scala ================================================ package scala.build.bsp import ch.epfl.scala.bsp4j as b import ch.epfl.scala.bsp4j.{BuildClient, LogMessageParams, MessageType} import java.io.{File, PrintWriter, StringWriter} import java.net.URI import java.util as ju import java.util.concurrent.{CompletableFuture, TimeUnit} import scala.build.Logger import scala.build.internal.Constants import scala.build.options.Scope import scala.concurrent.{Future, Promise} import scala.jdk.CollectionConverters.* import scala.util.Random class BspServer( bloopServer: b.BuildServer & b.ScalaBuildServer & b.JavaBuildServer & b.JvmBuildServer, compile: (() => CompletableFuture[b.CompileResult]) => CompletableFuture[b.CompileResult], logger: Logger, presetIntelliJ: Boolean = false ) extends BuildServerForwardStubs with ScalaScriptBuildServer with ScalaBuildServerForwardStubs with JavaBuildServerForwardStubs with JvmBuildServerForwardStubs with HasGeneratedSourcesImpl { private val client: Option[BuildClient] = None @volatile private var intelliJ: Boolean = presetIntelliJ def isIntelliJ: Boolean = intelliJ def clientOpt: Option[BuildClient] = client @volatile private var extraDependencySources: Seq[os.Path] = Nil def setExtraDependencySources(sourceJars: Seq[os.Path]): Unit = { extraDependencySources = sourceJars } @volatile private var extraTestDependencySources: Seq[os.Path] = Nil def setExtraTestDependencySources(sourceJars: Seq[os.Path]): Unit = { extraTestDependencySources = sourceJars } // Can we accept some errors in some circumstances? override protected def onFatalError(throwable: Throwable, context: String): Unit = { val sw = new StringWriter() throwable.printStackTrace(new PrintWriter(sw)) val message = s"Fatal error has occured within $context. Shutting down the server:\n ${sw.toString}" System.err.println(message) client.foreach(_.onBuildLogMessage(new LogMessageParams(MessageType.ERROR, message))) // wait random bit before shutting down server to reduce risk of multiple scala-cli instances starting bloop at the same time val timeout = Random.nextInt(400) TimeUnit.MILLISECONDS.sleep(100 + timeout) sys.exit(1) } private def maybeUpdateProjectTargetUri(res: b.WorkspaceBuildTargetsResult): Unit = for { (_, n) <- projectNames.iterator if n.targetUriOpt.isEmpty target <- res.getTargets.asScala.iterator.find(_.getDisplayName == n.name) } n.targetUriOpt = Some(target.getId.getUri) private def stripInvalidTargets(params: b.WorkspaceBuildTargetsResult): Unit = { val updatedTargets = params .getTargets .asScala .filter(target => validTarget(target.getId)) .asJava params.setTargets(updatedTargets) } private def check(params: b.CleanCacheParams): params.type = { val invalidTargets = params.getTargets.asScala.filter(!validTarget(_)) for (target <- invalidTargets) logger.debug(s"invalid target in CleanCache request: $target") params } private def check(params: b.CompileParams): params.type = { val invalidTargets = params.getTargets.asScala.filter(!validTarget(_)) for (target <- invalidTargets) logger.debug(s"invalid target in Compile request: $target") params } private def check(params: b.DependencySourcesParams): params.type = { val invalidTargets = params.getTargets.asScala.filter(!validTarget(_)) for (target <- invalidTargets) logger.debug(s"invalid target in DependencySources request: $target") params } private def check(params: b.ResourcesParams): params.type = { val invalidTargets = params.getTargets.asScala.filter(!validTarget(_)) for (target <- invalidTargets) logger.debug(s"invalid target in Resources request: $target") params } private def check(params: b.SourcesParams): params.type = { val invalidTargets = params.getTargets.asScala.filter(!validTarget(_)) for (target <- invalidTargets) logger.debug(s"invalid target in Sources request: $target") params } private def check(params: b.TestParams): params.type = { val invalidTargets = params.getTargets.asScala.filter(!validTarget(_)) for (target <- invalidTargets) logger.debug(s"invalid target in Test request: $target") params } private def check(params: b.DebugSessionParams): params.type = { val invalidTargets = params.getTargets.asScala.filter(!validTarget(_)) for (target <- invalidTargets) logger.debug(s"invalid target in Test request: $target") params } private def check(params: b.OutputPathsParams): params.type = { val invalidTargets = params.getTargets.asScala.filter(!validTarget(_)) for (target <- invalidTargets) logger.debug(s"invalid target in buildTargetOutputPaths request: $target") params } private def mapGeneratedSources(res: b.SourcesResult): Unit = { val gen = generatedSources.values.toVector for { item <- res.getItems.asScala if validTarget(item.getTarget) sourceItem <- item.getSources.asScala genSource <- gen.iterator.flatMap(_.uriMap.get(sourceItem.getUri).iterator).take(1) updatedUri <- genSource.reportingPath.toOption.map(_.toNIO.toUri.toASCIIString) } { sourceItem.setUri(updatedUri) sourceItem.setGenerated(false) } // GeneratedSources not corresponding to files that exist on disk (unlike script wrappers) val sourcesWithReportingPathString = generatedSources.values.flatMap(_.sources) .filter(_.reportingPath.isLeft) for { item <- res.getItems.asScala if validTarget(item.getTarget) sourceItem <- item.getSources.asScala if sourcesWithReportingPathString.exists( _.generated.toNIO.toUri.toASCIIString == sourceItem.getUri ) } sourceItem.setGenerated(true) } protected def forwardTo : b.BuildServer & b.ScalaBuildServer & b.JavaBuildServer & b.JvmBuildServer = bloopServer private val supportedLanguages: ju.List[String] = List( "scala", "java", // This makes Metals requests "wrapped sources" stuff, that makes it handle .sc files better. "scala-sc" ).asJava private def capabilities: b.BuildServerCapabilities = { val capabilities = new b.BuildServerCapabilities capabilities.setCompileProvider(new b.CompileProvider(supportedLanguages)) capabilities.setTestProvider(new b.TestProvider(supportedLanguages)) capabilities.setRunProvider(new b.RunProvider(supportedLanguages)) capabilities.setDebugProvider(new b.DebugProvider(supportedLanguages)) capabilities.setInverseSourcesProvider(true) capabilities.setDependencySourcesProvider(true) capabilities.setResourcesProvider(true) capabilities.setBuildTargetChangedProvider(true) capabilities.setJvmRunEnvironmentProvider(true) capabilities.setJvmTestEnvironmentProvider(true) capabilities.setCanReload(true) capabilities.setDependencyModulesProvider(true) capabilities.setOutputPathsProvider(true) capabilities } override def buildInitialize( params: b.InitializeBuildParams ): CompletableFuture[b.InitializeBuildResult] = { val res = new b.InitializeBuildResult( "scala-cli", Constants.version, bloop.rifle.internal.BuildInfo.bspVersion, capabilities ) val buildComesFromIntelliJ = params.getDisplayName.toLowerCase.contains("intellij") intelliJ = buildComesFromIntelliJ logger.debug(s"IntelliJ build: $buildComesFromIntelliJ") CompletableFuture.completedFuture(res) } override def onBuildInitialized(): Unit = () override def buildTargetCleanCache( params: b.CleanCacheParams ): CompletableFuture[b.CleanCacheResult] = super.buildTargetCleanCache(check(params)) override def buildTargetCompile(params: b.CompileParams): CompletableFuture[b.CompileResult] = compile(() => super.buildTargetCompile(check(params.withVerbosity(logger.verbosity > 0)))) override def buildTargetDependencySources( params: b.DependencySourcesParams ): CompletableFuture[b.DependencySourcesResult] = super.buildTargetDependencySources(check(params)).thenApply { res => val updatedItems = res.getItems.asScala.map { case item if validTarget(item.getTarget) => val isTestTarget = item.getTarget.getUri.endsWith("-test") val validExtraDependencySources = if isTestTarget then (extraDependencySources ++ extraTestDependencySources).distinct else extraDependencySources val updatedSources = item.getSources.asScala ++ validExtraDependencySources.map { sourceJar => sourceJar.toNIO.toUri.toASCIIString } new b.DependencySourcesItem(item.getTarget, updatedSources.asJava) case other => other } new b.DependencySourcesResult(updatedItems.asJava) } override def buildTargetResources( params: b.ResourcesParams ): CompletableFuture[b.ResourcesResult] = super.buildTargetResources(check(params)) override def buildTargetRun(params: b.RunParams): CompletableFuture[b.RunResult] = { val target = params.getTarget if (!validTarget(target)) logger.debug( s"Got invalid target in Run request: ${target.getUri} (expected ${targetScopeIdOpt(Scope.Main).orNull})" ) super.buildTargetRun(params) } override def buildTargetSources(params: b.SourcesParams): CompletableFuture[b.SourcesResult] = super.buildTargetSources(check(params)).thenApply { res => val res0 = res.duplicate() mapGeneratedSources(res0) res0 } override def buildTargetTest(params: b.TestParams): CompletableFuture[b.TestResult] = super.buildTargetTest(check(params)) override def debugSessionStart(params: b.DebugSessionParams) : CompletableFuture[b.DebugSessionAddress] = super.debugSessionStart(check(params)) override def buildTargetOutputPaths(params: b.OutputPathsParams) : CompletableFuture[b.OutputPathsResult] = { check(params) val targets = params.getTargets.asScala.filter(validTarget) val outputPathsItem = targets .map(buildTargetId => (buildTargetId, targetWorkspaceDirOpt(buildTargetId))) .collect { case (buildTargetId, Some(targetUri)) => (buildTargetId, targetUri) } .map { case (buildTargetId, targetUri) => new b.OutputPathsItem( buildTargetId, List(b.OutputPathItem(targetUri, b.OutputPathItemKind.DIRECTORY)).asJava ) } CompletableFuture.completedFuture(new b.OutputPathsResult(outputPathsItem.asJava)) } override def workspaceBuildTargets(): CompletableFuture[b.WorkspaceBuildTargetsResult] = super.workspaceBuildTargets().thenApply { res => maybeUpdateProjectTargetUri(res) val res0 = res.duplicate() stripInvalidTargets(res0) for (target <- res0.getTargets.asScala) { val capabilities = target.getCapabilities capabilities.setCanDebug(true) val baseDirectory = new File(new URI(target.getBaseDirectory)) if ( isIntelliJ && baseDirectory.getName == Constants.workspaceDirName && baseDirectory .getParentFile != null ) { val newBaseDirectory = baseDirectory.getParentFile.toPath.toUri.toASCIIString target.setBaseDirectory(newBaseDirectory) } } res0 } def buildTargetWrappedSources(params: WrappedSourcesParams) : CompletableFuture[WrappedSourcesResult] = { def sourcesItemOpt(scope: Scope) = targetScopeIdOpt(scope).map { id => val items = generatedSources .getOrElse(scope, HasGeneratedSources.GeneratedSources(Nil)) .sources .flatMap { s => s.reportingPath.toSeq.map(_.toNIO.toUri.toASCIIString).map { uri => val item = new WrappedSourceItem(uri, s.generated.toNIO.toUri.toASCIIString) val content = os.read(s.generated) item.setTopWrapper( content .linesIterator .take(s.wrapperParamsOpt.map(_.topWrapperLineCount).getOrElse(0)) .mkString("", System.lineSeparator(), System.lineSeparator()) ) item.setBottomWrapper("}") // meh item } } new WrappedSourcesItem(id, items.asJava) } val sourceItems = Seq(Scope.Main, Scope.Test).flatMap(sourcesItemOpt(_).toSeq) val res = new WrappedSourcesResult(sourceItems.asJava) CompletableFuture.completedFuture(res) } private val shutdownPromise = Promise[Unit]() override def buildShutdown(): CompletableFuture[Object] = { if (!shutdownPromise.isCompleted) shutdownPromise.success(()) CompletableFuture.completedFuture(null) } override def onBuildExit(): Unit = () def initiateShutdown: Future[Unit] = shutdownPromise.future } ================================================ FILE: modules/build/src/main/scala/scala/build/bsp/BspThreads.scala ================================================ package scala.build.bsp import java.util.concurrent.{ExecutorService, Executors} import scala.build.BuildThreads import scala.build.internal.Util final case class BspThreads( buildThreads: BuildThreads, prepareBuildExecutor: ExecutorService ) { def shutdown(): Unit = { buildThreads.shutdown() prepareBuildExecutor.shutdown() } } object BspThreads { def withThreads[T](f: BspThreads => T): T = { var threads: BspThreads = null try { threads = create() f(threads) } finally if (threads != null) threads.shutdown() } def create(): BspThreads = BspThreads( BuildThreads.create(), Executors.newSingleThreadExecutor( Util.daemonThreadFactory("scala-cli-bsp-prepare-build-thread") ) ) } ================================================ FILE: modules/build/src/main/scala/scala/build/bsp/BuildClientForwardStubs.scala ================================================ package scala.build.bsp import ch.epfl.scala.bsp4j as b trait BuildClientForwardStubs extends b.BuildClient { protected def forwardToOpt: Option[b.BuildClient] override def onBuildLogMessage(params: b.LogMessageParams): Unit = forwardToOpt.foreach(_.onBuildLogMessage(params)) override def onBuildPublishDiagnostics(params: b.PublishDiagnosticsParams): Unit = forwardToOpt.foreach(_.onBuildPublishDiagnostics(params)) override def onBuildShowMessage(params: b.ShowMessageParams): Unit = forwardToOpt.foreach(_.onBuildShowMessage(params)) override def onBuildTargetDidChange(params: b.DidChangeBuildTarget): Unit = forwardToOpt.foreach(_.onBuildTargetDidChange(params)) override def onBuildTaskFinish(params: b.TaskFinishParams): Unit = forwardToOpt.foreach(_.onBuildTaskFinish(params)) override def onBuildTaskProgress(params: b.TaskProgressParams): Unit = forwardToOpt.foreach(_.onBuildTaskProgress(params)) override def onBuildTaskStart(params: b.TaskStartParams): Unit = forwardToOpt.foreach(_.onBuildTaskStart(params)) } ================================================ FILE: modules/build/src/main/scala/scala/build/bsp/BuildServerForwardStubs.scala ================================================ package scala.build.bsp import ch.epfl.scala.bsp4j as b import ch.epfl.scala.bsp4j.{DependencyModulesParams, DependencyModulesResult} import java.util.concurrent.CompletableFuture import java.util.function.BiFunction trait BuildServerForwardStubs extends b.BuildServer { protected def forwardTo: b.BuildServer protected def onFatalError(throwable: Throwable, context: String): Unit def fatalExceptionHandler[T](methodName: String, params: Any*) = new BiFunction[T, Throwable, T] { override def apply(maybeValue: T, maybeException: Throwable): T = maybeException match { case null => maybeValue case error => val methodContext = s"bloop bsp server, method: $methodName" val context = if (params.isEmpty) methodContext else params.mkString(s"$methodContext, with params: ", ", ", "") onFatalError(error, context) throw error } } override def buildShutdown(): CompletableFuture[Object] = forwardTo.buildShutdown().handle(fatalExceptionHandler("buildShutdown")) override def buildTargetCleanCache( params: b.CleanCacheParams ): CompletableFuture[b.CleanCacheResult] = forwardTo.buildTargetCleanCache(params) .handle(fatalExceptionHandler("buildTargetCleanCache", params)) override def buildTargetCompile(params: b.CompileParams): CompletableFuture[b.CompileResult] = forwardTo.buildTargetCompile(params) .handle(fatalExceptionHandler("buildTargetCompile", params)) override def buildTargetDependencySources( params: b.DependencySourcesParams ): CompletableFuture[b.DependencySourcesResult] = forwardTo.buildTargetDependencySources(params) .handle(fatalExceptionHandler("buildTargetDependencySources", params)) override def buildTargetInverseSources( params: b.InverseSourcesParams ): CompletableFuture[b.InverseSourcesResult] = forwardTo.buildTargetInverseSources(params) .handle(fatalExceptionHandler("buildTargetInverseSources", params)) override def buildTargetResources( params: b.ResourcesParams ): CompletableFuture[b.ResourcesResult] = forwardTo.buildTargetResources(params) .handle(fatalExceptionHandler("buildTargetResources", params)) override def buildTargetRun(params: b.RunParams): CompletableFuture[b.RunResult] = forwardTo.buildTargetRun(params) .handle(fatalExceptionHandler("buildTargetRun", params)) override def buildTargetSources(params: b.SourcesParams): CompletableFuture[b.SourcesResult] = forwardTo.buildTargetSources(params) .handle(fatalExceptionHandler("buildTargetSources", params)) override def buildTargetTest(params: b.TestParams): CompletableFuture[b.TestResult] = forwardTo.buildTargetTest(params) .handle(fatalExceptionHandler("buildTargetTest", params)) override def debugSessionStart(params: b.DebugSessionParams) : CompletableFuture[b.DebugSessionAddress] = forwardTo.debugSessionStart(params) .handle(fatalExceptionHandler("debugSessionStart", params)) override def workspaceBuildTargets(): CompletableFuture[b.WorkspaceBuildTargetsResult] = forwardTo.workspaceBuildTargets() .handle(fatalExceptionHandler("workspaceBuildTargets")) /** This implementation should never be called and is merely a placeholder. As Bloop doesn't * support reloading its workspace, Scala CLI has to reload Bloop instead. And so, * [[BuildServerProxy]].workspaceReload() is responsible for the actual reload. */ override def workspaceReload(): CompletableFuture[Object] = CompletableFuture.completedFuture(new Object) // should never be called, as per scaladoc override def buildTargetDependencyModules(params: DependencyModulesParams) : CompletableFuture[DependencyModulesResult] = forwardTo.buildTargetDependencyModules(params) .handle(fatalExceptionHandler("buildTargetDependencyModules", params)) } ================================================ FILE: modules/build/src/main/scala/scala/build/bsp/BuildServerProxy.scala ================================================ package scala.build.bsp import ch.epfl.scala.bsp4j as b import java.util.concurrent.CompletableFuture import scala.build.GeneratedSource import scala.build.input.Inputs import scala.build.options.Scope /** A wrapper for [[BspServer]], allowing to reload the workspace on the fly. * @param bspServer * the underlying BSP server relying on Bloop * @param onReload * the actual `workspace/reload` function */ class BuildServerProxy( bspServer: () => BspServer, onReload: () => CompletableFuture[Object] ) extends b.BuildServer with b.ScalaBuildServer with b.JavaBuildServer with b.JvmBuildServer with ScalaScriptBuildServer with HasGeneratedSources { override def buildInitialize(params: b.InitializeBuildParams) : CompletableFuture[b.InitializeBuildResult] = bspServer().buildInitialize(params) override def onBuildInitialized(): Unit = bspServer().onBuildInitialized() override def buildShutdown(): CompletableFuture[AnyRef] = bspServer().buildShutdown() override def onBuildExit(): Unit = bspServer().onBuildExit() override def workspaceBuildTargets(): CompletableFuture[b.WorkspaceBuildTargetsResult] = bspServer().workspaceBuildTargets() override def buildTargetSources(params: b.SourcesParams): CompletableFuture[b.SourcesResult] = bspServer().buildTargetSources(params) override def buildTargetInverseSources(params: b.InverseSourcesParams) : CompletableFuture[b.InverseSourcesResult] = bspServer().buildTargetInverseSources(params) override def buildTargetDependencySources(params: b.DependencySourcesParams) : CompletableFuture[b.DependencySourcesResult] = bspServer().buildTargetDependencySources(params) override def buildTargetResources(params: b.ResourcesParams) : CompletableFuture[b.ResourcesResult] = bspServer().buildTargetResources(params) override def buildTargetCompile(params: b.CompileParams): CompletableFuture[b.CompileResult] = bspServer().buildTargetCompile(params) override def buildTargetTest(params: b.TestParams): CompletableFuture[b.TestResult] = bspServer().buildTargetTest(params) override def buildTargetRun(params: b.RunParams): CompletableFuture[b.RunResult] = bspServer().buildTargetRun(params) override def buildTargetCleanCache(params: b.CleanCacheParams) : CompletableFuture[b.CleanCacheResult] = bspServer().buildTargetCleanCache(params) override def buildTargetDependencyModules(params: b.DependencyModulesParams) : CompletableFuture[b.DependencyModulesResult] = bspServer().buildTargetDependencyModules(params) override def buildTargetScalacOptions(params: b.ScalacOptionsParams) : CompletableFuture[b.ScalacOptionsResult] = bspServer().buildTargetScalacOptions(params) @deprecated override def buildTargetScalaTestClasses(params: b.ScalaTestClassesParams) : CompletableFuture[b.ScalaTestClassesResult] = bspServer().buildTargetScalaTestClasses(params) @deprecated override def buildTargetScalaMainClasses(params: b.ScalaMainClassesParams) : CompletableFuture[b.ScalaMainClassesResult] = bspServer().buildTargetScalaMainClasses(params) override def buildTargetJavacOptions(params: b.JavacOptionsParams) : CompletableFuture[b.JavacOptionsResult] = bspServer().buildTargetJavacOptions(params) override def debugSessionStart(params: b.DebugSessionParams) : CompletableFuture[b.DebugSessionAddress] = bspServer().debugSessionStart(params) override def buildTargetWrappedSources(params: WrappedSourcesParams) : CompletableFuture[WrappedSourcesResult] = bspServer().buildTargetWrappedSources(params) override def buildTargetOutputPaths(params: b.OutputPathsParams) : CompletableFuture[b.OutputPathsResult] = bspServer().buildTargetOutputPaths(params) /** As Bloop doesn't support `workspace/reload` requests and we have to reload it on Scala CLI's * end, this is used instead of [[BspServer]]'s [[BuildServerForwardStubs]].workspaceReload(). */ override def workspaceReload(): CompletableFuture[AnyRef] = onReload() override def buildTargetJvmRunEnvironment(params: b.JvmRunEnvironmentParams) : CompletableFuture[b.JvmRunEnvironmentResult] = bspServer().buildTargetJvmRunEnvironment(params) override def buildTargetJvmTestEnvironment(params: b.JvmTestEnvironmentParams) : CompletableFuture[b.JvmTestEnvironmentResult] = bspServer().buildTargetJvmTestEnvironment(params) def targetIds: List[b.BuildTargetIdentifier] = bspServer().targetIds def targetScopeIdOpt(scope: Scope): Option[b.BuildTargetIdentifier] = bspServer().targetScopeIdOpt(scope) def setGeneratedSources(scope: Scope, sources: Seq[GeneratedSource]): Unit = bspServer().setGeneratedSources(scope, sources) def setProjectName(workspace: os.Path, name: String, scope: Scope): Unit = bspServer().setProjectName(workspace, name, scope) def resetProjectNames(): Unit = bspServer().resetProjectNames() def newInputs(inputs: Inputs): Unit = bspServer().newInputs(inputs) } ================================================ FILE: modules/build/src/main/scala/scala/build/bsp/HasGeneratedSources.scala ================================================ package scala.build.bsp import ch.epfl.scala.bsp4j as b import scala.build.GeneratedSource import scala.build.input.Inputs import scala.build.internal.Constants import scala.build.options.Scope trait HasGeneratedSources { def targetIds: List[b.BuildTargetIdentifier] def targetScopeIdOpt(scope: Scope): Option[b.BuildTargetIdentifier] def setProjectName(workspace: os.Path, name: String, scope: Scope): Unit def resetProjectNames(): Unit def newInputs(inputs: Inputs): Unit def setGeneratedSources(scope: Scope, sources: Seq[GeneratedSource]): Unit } object HasGeneratedSources { final case class GeneratedSources( sources: Seq[GeneratedSource] ) { lazy val uriMap: Map[String, GeneratedSource] = sources .flatMap { g => g.reportingPath match { case Left(_) => Nil case Right(_) => Seq(g.generated.toNIO.toUri.toASCIIString -> g) } } .toMap } final case class ProjectName( bloopWorkspace: os.Path, name: String, var targetUriOpt: Option[String] = None ) { targetUriOpt = Some( (bloopWorkspace / Constants.workspaceDirName) .toIO .toURI .toASCIIString .stripSuffix("/") + "/?id=" + name ) } } ================================================ FILE: modules/build/src/main/scala/scala/build/bsp/HasGeneratedSourcesImpl.scala ================================================ package scala.build.bsp import ch.epfl.scala.bsp4j as b import scala.build.GeneratedSource import scala.build.input.Inputs import scala.build.internal.Constants import scala.build.options.Scope import scala.collection.mutable trait HasGeneratedSourcesImpl extends HasGeneratedSources { import HasGeneratedSources._ protected val projectNames = mutable.Map[Scope, ProjectName]() protected val generatedSources = mutable.Map[Scope, GeneratedSources]() def targetIds: List[b.BuildTargetIdentifier] = projectNames .toList .sortBy(_._1) .map(_._2) .flatMap(_.targetUriOpt) .map(uri => new b.BuildTargetIdentifier(uri)) def targetScopeIdOpt(scope: Scope): Option[b.BuildTargetIdentifier] = projectNames .get(scope) .flatMap(_.targetUriOpt) .map(uri => new b.BuildTargetIdentifier(uri)) def resetProjectNames(): Unit = projectNames.clear() def setProjectName(workspace: os.Path, name: String, scope: Scope): Unit = if (!projectNames.contains(scope)) projectNames(scope) = ProjectName(workspace, name) def newInputs(inputs: Inputs): Unit = { resetProjectNames() setProjectName(inputs.workspace, inputs.projectName, Scope.Main) setProjectName(inputs.workspace, inputs.scopeProjectName(Scope.Test), Scope.Test) } def setGeneratedSources(scope: Scope, sources: Seq[GeneratedSource]): Unit = { generatedSources(scope) = GeneratedSources(sources) } protected def targetWorkspaceDirOpt(id: b.BuildTargetIdentifier): Option[String] = projectNames.collectFirst { case (_, projName) if projName.targetUriOpt.contains(id.getUri) => (projName.bloopWorkspace / Constants.workspaceDirName).toIO.toURI.toASCIIString } protected def targetScopeOpt(id: b.BuildTargetIdentifier): Option[Scope] = projectNames.collectFirst { case (scope, projName) if projName.targetUriOpt.contains(id.getUri) => scope } protected def validTarget(id: b.BuildTargetIdentifier): Boolean = targetScopeOpt(id).nonEmpty } ================================================ FILE: modules/build/src/main/scala/scala/build/bsp/IdeInputs.scala ================================================ package scala.build.bsp import com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker final case class IdeInputs(args: Seq[String]) object IdeInputs { implicit lazy val codec: JsonValueCodec[IdeInputs] = JsonCodecMaker.make } ================================================ FILE: modules/build/src/main/scala/scala/build/bsp/JavaBuildServerForwardStubs.scala ================================================ package scala.build.bsp import ch.epfl.scala.bsp4j as b import ch.epfl.scala.bsp4j.{JavacOptionsParams, JavacOptionsResult} import java.util.concurrent.CompletableFuture trait JavaBuildServerForwardStubs extends b.JavaBuildServer { protected def forwardTo: b.JavaBuildServer override def buildTargetJavacOptions( params: JavacOptionsParams ): CompletableFuture[JavacOptionsResult] = forwardTo.buildTargetJavacOptions(params) } ================================================ FILE: modules/build/src/main/scala/scala/build/bsp/JsonRpcErrorCodes.scala ================================================ package scala.build.bsp /** Response error codes as defined in JSON RPC. * [[https://www.jsonrpc.org/specification#error_object]] */ object JsonRpcErrorCodes { val ParseError: Int = -32700 // Invalid JSON was received by the server. val InvalidRequest: Int = -32600 // The JSON sent is not a valid Request object. val MethodNotFound: Int = -32601 // The method does not exist / is not available. val InvalidParams: Int = -32602 // Invalid method parameter(s). val InternalError: Int = -32603 // Internal JSON-RPC error. } ================================================ FILE: modules/build/src/main/scala/scala/build/bsp/JvmBuildServerForwardStubs.scala ================================================ package scala.build.bsp import ch.epfl.scala.bsp4j as b import java.util.concurrent.CompletableFuture trait JvmBuildServerForwardStubs extends b.JvmBuildServer { protected def forwardTo: b.JvmBuildServer override def buildTargetJvmRunEnvironment(params: b.JvmRunEnvironmentParams) : CompletableFuture[b.JvmRunEnvironmentResult] = forwardTo.buildTargetJvmRunEnvironment(params) override def buildTargetJvmTestEnvironment(params: b.JvmTestEnvironmentParams) : CompletableFuture[b.JvmTestEnvironmentResult] = forwardTo.buildTargetJvmTestEnvironment(params) } ================================================ FILE: modules/build/src/main/scala/scala/build/bsp/LoggingBuildClient.scala ================================================ package scala.build.bsp import ch.epfl.scala.bsp4j as b trait LoggingBuildClient extends b.BuildClient { protected def underlying: b.BuildClient override def onBuildLogMessage(params: b.LogMessageParams): Unit = underlying.onBuildLogMessage(pprint.err.log(params)) override def onBuildPublishDiagnostics(params: b.PublishDiagnosticsParams): Unit = underlying.onBuildPublishDiagnostics(pprint.err.log(params)) override def onBuildShowMessage(params: b.ShowMessageParams): Unit = underlying.onBuildShowMessage(pprint.err.log(params)) override def onBuildTargetDidChange(params: b.DidChangeBuildTarget): Unit = underlying.onBuildTargetDidChange(pprint.err.log(params)) override def onBuildTaskFinish(params: b.TaskFinishParams): Unit = underlying.onBuildTaskFinish(pprint.err.log(params)) override def onBuildTaskProgress(params: b.TaskProgressParams): Unit = underlying.onBuildTaskProgress(pprint.err.log(params)) override def onBuildTaskStart(params: b.TaskStartParams): Unit = underlying.onBuildTaskStart(pprint.err.log(params)) } ================================================ FILE: modules/build/src/main/scala/scala/build/bsp/LoggingBuildServer.scala ================================================ package scala.build.bsp import ch.epfl.scala.bsp4j as b import ch.epfl.scala.bsp4j.{DependencyModulesParams, DependencyModulesResult} import java.util.concurrent.CompletableFuture trait LoggingBuildServer extends b.BuildServer { protected def underlying: b.BuildServer override def buildInitialize( params: b.InitializeBuildParams ): CompletableFuture[b.InitializeBuildResult] = underlying.buildInitialize(pprint.err.log(params)).logF override def onBuildExit(): Unit = underlying.onBuildExit() override def onBuildInitialized(): Unit = underlying.onBuildInitialized() override def buildShutdown(): CompletableFuture[Object] = underlying.buildShutdown().logF override def buildTargetCleanCache( params: b.CleanCacheParams ): CompletableFuture[b.CleanCacheResult] = underlying.buildTargetCleanCache(pprint.err.log(params)).logF override def buildTargetCompile(params: b.CompileParams): CompletableFuture[b.CompileResult] = underlying.buildTargetCompile(pprint.err.log(params)).logF override def buildTargetDependencySources( params: b.DependencySourcesParams ): CompletableFuture[b.DependencySourcesResult] = underlying.buildTargetDependencySources(pprint.err.log(params)).logF override def buildTargetInverseSources( params: b.InverseSourcesParams ): CompletableFuture[b.InverseSourcesResult] = underlying.buildTargetInverseSources(pprint.err.log(params)).logF override def buildTargetResources( params: b.ResourcesParams ): CompletableFuture[b.ResourcesResult] = underlying.buildTargetResources(pprint.err.log(params)).logF override def buildTargetRun(params: b.RunParams): CompletableFuture[b.RunResult] = underlying.buildTargetRun(pprint.err.log(params)).logF override def buildTargetSources(params: b.SourcesParams): CompletableFuture[b.SourcesResult] = underlying.buildTargetSources(pprint.err.log(params)).logF override def buildTargetTest(params: b.TestParams): CompletableFuture[b.TestResult] = underlying.buildTargetTest(pprint.err.log(params)).logF override def debugSessionStart(params: b.DebugSessionParams) : CompletableFuture[b.DebugSessionAddress] = underlying.debugSessionStart(pprint.err.log(params)).logF override def workspaceBuildTargets(): CompletableFuture[b.WorkspaceBuildTargetsResult] = underlying.workspaceBuildTargets().logF override def workspaceReload(): CompletableFuture[Object] = underlying.workspaceReload().logF override def buildTargetDependencyModules(params: DependencyModulesParams) : CompletableFuture[DependencyModulesResult] = underlying.buildTargetDependencyModules(pprint.err.log(params)).logF } ================================================ FILE: modules/build/src/main/scala/scala/build/bsp/LoggingBuildServerAll.scala ================================================ package scala.build.bsp import ch.epfl.scala.bsp4j as b import java.util.concurrent.CompletableFuture class LoggingBuildServerAll( val underlying: b.BuildServer & b.ScalaBuildServer & b.JavaBuildServer & b.JvmBuildServer & ScalaScriptBuildServer ) extends LoggingBuildServer with LoggingScalaBuildServer with LoggingJavaBuildServer with LoggingJvmBuildServer with ScalaScriptBuildServer { def buildTargetWrappedSources(params: WrappedSourcesParams) : CompletableFuture[WrappedSourcesResult] = underlying.buildTargetWrappedSources(pprint.err.log(params)).logF override def buildTargetOutputPaths(params: b.OutputPathsParams) : CompletableFuture[b.OutputPathsResult] = underlying.buildTargetOutputPaths(pprint.err.log(params)).logF } ================================================ FILE: modules/build/src/main/scala/scala/build/bsp/LoggingJavaBuildServer.scala ================================================ package scala.build.bsp import ch.epfl.scala.bsp4j as b import java.util.concurrent.CompletableFuture trait LoggingJavaBuildServer extends b.JavaBuildServer { protected def underlying: b.JavaBuildServer override def buildTargetJavacOptions( params: b.JavacOptionsParams ): CompletableFuture[b.JavacOptionsResult] = underlying.buildTargetJavacOptions(pprint.err.log(params)).logF } ================================================ FILE: modules/build/src/main/scala/scala/build/bsp/LoggingJvmBuildServer.scala ================================================ package scala.build.bsp import ch.epfl.scala.bsp4j as b import java.util.concurrent.CompletableFuture trait LoggingJvmBuildServer extends b.JvmBuildServer { protected def underlying: b.JvmBuildServer override def buildTargetJvmRunEnvironment(params: b.JvmRunEnvironmentParams) : CompletableFuture[b.JvmRunEnvironmentResult] = underlying.buildTargetJvmRunEnvironment(pprint.err.log(params)).logF override def buildTargetJvmTestEnvironment(params: b.JvmTestEnvironmentParams) : CompletableFuture[b.JvmTestEnvironmentResult] = underlying.buildTargetJvmTestEnvironment(pprint.err.log(params)).logF } ================================================ FILE: modules/build/src/main/scala/scala/build/bsp/LoggingScalaBuildServer.scala ================================================ package scala.build.bsp import ch.epfl.scala.bsp4j as b import java.util.concurrent.CompletableFuture trait LoggingScalaBuildServer extends b.ScalaBuildServer { protected def underlying: b.ScalaBuildServer @deprecated override def buildTargetScalaMainClasses( params: b.ScalaMainClassesParams ): CompletableFuture[b.ScalaMainClassesResult] = underlying.buildTargetScalaMainClasses(pprint.err.log(params)).logF @deprecated override def buildTargetScalaTestClasses( params: b.ScalaTestClassesParams ): CompletableFuture[b.ScalaTestClassesResult] = underlying.buildTargetScalaTestClasses(pprint.err.log(params)).logF override def buildTargetScalacOptions( params: b.ScalacOptionsParams ): CompletableFuture[b.ScalacOptionsResult] = underlying.buildTargetScalacOptions(pprint.err.log(params)).logF } ================================================ FILE: modules/build/src/main/scala/scala/build/bsp/ScalaBuildServerForwardStubs.scala ================================================ package scala.build.bsp import ch.epfl.scala.bsp4j as b import java.util.concurrent.CompletableFuture trait ScalaBuildServerForwardStubs extends b.ScalaBuildServer { protected def forwardTo: b.ScalaBuildServer @deprecated override def buildTargetScalaMainClasses( params: b.ScalaMainClassesParams ): CompletableFuture[b.ScalaMainClassesResult] = forwardTo.buildTargetScalaMainClasses(params) @deprecated override def buildTargetScalaTestClasses( params: b.ScalaTestClassesParams ): CompletableFuture[b.ScalaTestClassesResult] = forwardTo.buildTargetScalaTestClasses(params) override def buildTargetScalacOptions( params: b.ScalacOptionsParams ): CompletableFuture[b.ScalacOptionsResult] = forwardTo.buildTargetScalacOptions(params) } ================================================ FILE: modules/build/src/main/scala/scala/build/bsp/package.scala ================================================ package scala.build import ch.epfl.scala.bsp4j as b import ch.epfl.scala.bsp4j.SourcesItem import java.util.concurrent.CompletableFuture import scala.jdk.CollectionConverters.* package object bsp { extension (compileParams: b.CompileParams) { def duplicate(): b.CompileParams = { val params = new b.CompileParams(compileParams.getTargets) params.setOriginId(compileParams.getOriginId) params.setArguments(compileParams.getArguments) params } def withExtraArgs(extraArgs: List[String]): b.CompileParams = { val params = compileParams.duplicate() val previousArguments = Option(params.getArguments).toList.flatMap(_.asScala) val newArguments = (previousArguments ++ extraArgs).asJava params.setArguments(newArguments) params } def withVerbosity(verbose: Boolean): b.CompileParams = { val verboseArg = "--verbose" val argumentsContainVerboseArg = Option(compileParams.getArguments).exists(_.contains(verboseArg)) if verbose && !argumentsContainVerboseArg then compileParams.withExtraArgs(List(verboseArg)) else compileParams } } implicit class Ext[T](private val f: CompletableFuture[T]) extends AnyVal { def logF: CompletableFuture[T] = f.handle { (res, err) => if (err == null) pprint.err.log(res) else throw pprint.err.log(err) } } implicit class BuildTargetIdentifierExt( private val item: b.BuildTargetIdentifier ) extends AnyVal { def duplicate(): b.BuildTargetIdentifier = new b.BuildTargetIdentifier(item.getUri) } implicit class SourceItemExt(private val item: b.SourceItem) extends AnyVal { def duplicate(): b.SourceItem = new b.SourceItem(item.getUri, item.getKind, item.getGenerated) } implicit class SourcesItemExt(private val item: b.SourcesItem) extends AnyVal { def duplicate(): b.SourcesItem = { val other = new b.SourcesItem( item.getTarget, item.getSources.asScala.map(_.duplicate()).asJava ) for (roots <- Option(item.getRoots)) other.setRoots(roots.asScala.toList.asJava) other } } implicit class SourcesResultExt(private val res: b.SourcesResult) extends AnyVal { def duplicate(): b.SourcesResult = new b.SourcesResult( res.getItems().asScala.toList .map((item: SourcesItem) => item.duplicate()).asJava ) } implicit class BuildTargetCapabilitiesExt( private val capabilities: b.BuildTargetCapabilities ) extends AnyVal { def duplicate(): b.BuildTargetCapabilities = val buildTarget = new b.BuildTargetCapabilities() buildTarget.setCanCompile(capabilities.getCanCompile) buildTarget.setCanTest(capabilities.getCanTest) buildTarget.setCanRun(capabilities.getCanRun) buildTarget } implicit class BuildTargetExt(private val target: b.BuildTarget) extends AnyVal { def duplicate(): b.BuildTarget = { val target0 = new b.BuildTarget( target.getId.duplicate(), target.getTags.asScala.toList.asJava, target.getLanguageIds.asScala.toList.asJava, target.getDependencies.asScala.toList.map(_.duplicate()).asJava, target.getCapabilities.duplicate() ) target0.setBaseDirectory(target.getBaseDirectory) target0.setData(target.getData) // FIXME Duplicate this when we can too? target0.setDataKind(target.getDataKind) target0.setDisplayName(target.getDisplayName) target0 } } implicit class WorkspaceBuildTargetsResultExt( private val res: b.WorkspaceBuildTargetsResult ) extends AnyVal { def duplicate(): b.WorkspaceBuildTargetsResult = new b.WorkspaceBuildTargetsResult(res.getTargets.asScala.toList.map(_.duplicate()).asJava) } implicit class LocationExt(private val loc: b.Location) extends AnyVal { def duplicate(): b.Location = new b.Location(loc.getUri, loc.getRange.duplicate()) } implicit class DiagnosticRelatedInformationExt( private val info: b.DiagnosticRelatedInformation ) extends AnyVal { def duplicate(): b.DiagnosticRelatedInformation = new b.DiagnosticRelatedInformation(info.getLocation.duplicate(), info.getMessage) } implicit class PositionExt(private val pos: b.Position) extends AnyVal { def duplicate(): b.Position = new b.Position(pos.getLine, pos.getCharacter) } implicit class RangeExt(private val range: b.Range) extends AnyVal { def duplicate(): b.Range = new b.Range(range.getStart.duplicate(), range.getEnd.duplicate()) } implicit class DiagnosticExt(private val diag: b.Diagnostic) extends AnyVal { def duplicate(): b.Diagnostic = { val diag0 = new b.Diagnostic(diag.getRange.duplicate(), diag.getMessage) diag0.setCode(diag.getCode) diag0.setRelatedInformation( Option(diag.getRelatedInformation).map(_.asScala.map(_.duplicate()).asJava).orNull ) diag0.setSeverity(diag.getSeverity) diag0.setSource(diag.getSource) diag0.setData(diag.getData) diag0 } } } ================================================ FILE: modules/build/src/main/scala/scala/build/bsp/protocol/TextEdit.scala ================================================ package scala.build.bsp.protocol import ch.epfl.scala.bsp4j as b import com.google.gson.Gson case class TextEdit(range: b.Range, newText: String) { def toJsonTree() = new Gson().toJsonTree(this) } ================================================ FILE: modules/build/src/main/scala/scala/build/compiler/BloopCompiler.scala ================================================ package scala.build.compiler import scala.annotation.tailrec import scala.build.{Bloop, Logger, Position, Positioned, Project} import scala.concurrent.duration.FiniteDuration final class BloopCompiler( createServer: () => bloop.rifle.BloopServer, buildTargetsTimeout: FiniteDuration, strictBloopJsonCheck: Boolean ) extends ScalaCompiler { private var currentBloopServer: bloop.rifle.BloopServer = createServer() def bloopServer: bloop.rifle.BloopServer = currentBloopServer def jvmVersion: Option[Positioned[Int]] = Some( Positioned( List(Position.Bloop(bloopServer.bloopInfo.javaHome)), bloopServer.bloopInfo.jvmVersion ) ) def prepareProject( project: Project, logger: Logger ): Boolean = project.writeBloopFile(strictBloopJsonCheck, logger) def compile( project: Project, logger: Logger ): Boolean = { @tailrec def helper(remainingAttempts: Int): Boolean = Bloop.compile(project.projectName, bloopServer.server, logger, buildTargetsTimeout) match { case Right(res) => res case Left(ex) => if (remainingAttempts > 1) { logger.debug(s"Seems Bloop server exited (got $ex), trying to restart one") currentBloopServer = createServer() helper(remainingAttempts - 1) } else throw new Exception( "Seems compilation server exited, and wasn't able to restart one", ex ) } helper(2) } def shutdown(): Unit = bloopServer.shutdown() } ================================================ FILE: modules/build/src/main/scala/scala/build/compiler/BloopCompilerMaker.scala ================================================ package scala.build.compiler import bloop.rifle.{BloopRifleConfig, BloopServer, BloopThreads} import ch.epfl.scala.bsp4j.BuildClient import scala.build.errors.{BuildException, FetchingDependenciesError, Severity} import scala.build.internal.Constants import scala.build.internal.util.WarningMessages import scala.build.options.BuildOptions import scala.build.{Logger, retry} import scala.concurrent.duration.DurationInt import scala.util.Try final class BloopCompilerMaker( getConfig: BuildOptions => Either[BuildException, BloopRifleConfig], threads: BloopThreads, strictBloopJsonCheck: Boolean, offline: Boolean ) extends ScalaCompilerMaker { def create( workspace: os.Path, classesDir: os.Path, buildClient: BuildClient, logger: Logger, buildOptions: BuildOptions ): Either[BuildException, ScalaCompiler] = getConfig(buildOptions) match case Left(_) if offline => logger.diagnostic(WarningMessages.offlineModeBloopJvmNotFound, Severity.Warning) SimpleScalaCompilerMaker("java", Nil).create( workspace, classesDir, buildClient, logger, buildOptions ) case Right(config) => val createBuildServer = () => // retrying here in case a number of Scala CLI processes are started at the same time // and they all try to connect to the server / spawn a new server // otherwise, users may run into one of: // - libdaemonjvm.client.ConnectError$ZombieFound // - Caught java.lang.RuntimeException: Fatal error, could not spawn Bloop: not running // - java.lang.RuntimeException: Bloop BSP connection in (...) was unexpectedly closed or bloop didn't start. // if a sufficiently large number of processes was started, this may happen anyway, of course retry(if offline then 1 else 3)(logger) { BloopServer.buildServer( config, "scala-cli", Constants.version, workspace.toNIO, classesDir.toNIO, buildClient, threads, logger.bloopRifleLogger ) } val res = Try(new BloopCompiler(createBuildServer, 20.seconds, strictBloopJsonCheck)) .toEither .left.flatMap { case e if offline => e.getCause match case _: FetchingDependenciesError => logger.diagnostic( WarningMessages.offlineModeBloopNotFound, Severity.Warning ) SimpleScalaCompilerMaker("java", Nil) .create(workspace, classesDir, buildClient, logger, buildOptions) case _ => Left(e) case e => Left(e) }.fold(t => throw t, identity) Right(res) case Left(ex) => Left(ex) } ================================================ FILE: modules/build/src/main/scala/scala/build/compiler/ScalaCompiler.scala ================================================ package scala.build.compiler import scala.build.{Logger, Positioned, Project} trait ScalaCompiler { def jvmVersion: Option[Positioned[Int]] def prepareProject( project: Project, logger: Logger ): Boolean def compile( project: Project, logger: Logger ): Boolean def shutdown(): Unit def usesClassDir: Boolean = true } object ScalaCompiler { final case class IgnoreScala2(compiler: ScalaCompiler) extends ScalaCompiler { private def ignore( project: Project, logger: Logger ): Boolean = project.scalaCompiler.exists { scalaCompiler0 => val scalaVer = scalaCompiler0.scalaVersion val isScala2 = scalaVer.startsWith("2.") logger.debug(s"Ignoring compilation for Scala version $scalaVer") isScala2 } def jvmVersion: Option[Positioned[Int]] = compiler.jvmVersion def prepareProject( project: Project, logger: Logger ): Boolean = ignore(project, logger) || compiler.prepareProject(project, logger) def compile( project: Project, logger: Logger ): Boolean = ignore(project, logger) || compiler.compile(project, logger) def shutdown(): Unit = compiler.shutdown() override def usesClassDir: Boolean = compiler.usesClassDir } } ================================================ FILE: modules/build/src/main/scala/scala/build/compiler/ScalaCompilerMaker.scala ================================================ package scala.build.compiler import ch.epfl.scala.bsp4j.BuildClient import scala.build.Logger import scala.build.errors.BuildException import scala.build.options.BuildOptions trait ScalaCompilerMaker { def create( workspace: os.Path, classesDir: os.Path, buildClient: BuildClient, logger: Logger, buildOptions: BuildOptions ): Either[BuildException, ScalaCompiler] final def withCompiler[T]( workspace: os.Path, classesDir: os.Path, buildClient: BuildClient, logger: Logger, buildOptions: BuildOptions )( f: ScalaCompiler => Either[BuildException, T] ): Either[BuildException, T] = { var server: ScalaCompiler = null try { val createdServer = create( workspace, classesDir, buildClient, logger, buildOptions ) server = createdServer.toOption.getOrElse(null) createdServer.flatMap(f) } // format: off finally { if (server != null) server.shutdown() } // format: on } } object ScalaCompilerMaker { final case class IgnoreScala2(compilerMaker: ScalaCompilerMaker) extends ScalaCompilerMaker { def create( workspace: os.Path, classesDir: os.Path, buildClient: BuildClient, logger: Logger, buildOptions: BuildOptions ): Either[BuildException, ScalaCompiler] = compilerMaker.create(workspace, classesDir, buildClient, logger, buildOptions).map( ScalaCompiler.IgnoreScala2(_) ) } } ================================================ FILE: modules/build/src/main/scala/scala/build/compiler/SimpleJavaCompiler.scala ================================================ package scala.build.compiler import java.io.File import scala.build.internal.Runner import scala.build.{Logger, Project} import scala.util.Properties /** A simple Java compiler to handle pure Java projects. * * @param defaultJavaCommand * the default `java` command to be used * @param defaultJavaOptions * the default jvm options to be used with the `java` command */ final case class SimpleJavaCompiler( defaultJavaCommand: String, defaultJavaOptions: Seq[String] ) { def compile( project: Project, logger: Logger ): Boolean = project.sources.isEmpty || { val javacCommand = project.javaHomeOpt.map(javaHome => SimpleJavaCompiler.javaCommand(javaHome, "javac")) .getOrElse(defaultJavaCommand) // Java 8 doesn't automatically create a directory for the classes if (!os.exists(project.classesDir)) os.makeDir.all(project.classesDir) val args = project.javacOptions ++ Seq( "-d", project.classesDir.toString, "-cp", project.classPath.map(_.toString).mkString(File.pathSeparator) ) ++ project.sources.map(_.toString) val proc = Runner.run( Seq(javacCommand) ++ args, logger, cwd = Some(project.workspace) ) val res = proc.waitFor() res == 0 } } object SimpleJavaCompiler { def javaCommand(javaHome: os.Path, command: String = "java"): String = { val ext = if (Properties.isWin) ".exe" else "" val path = javaHome / "bin" / s"$command$ext" path.toString } } ================================================ FILE: modules/build/src/main/scala/scala/build/compiler/SimpleScalaCompiler.scala ================================================ package scala.build.compiler import java.io.File import scala.build.internal.{Constants, Runner} import scala.build.{Logger, Positioned, Project} /** A simple Scala compiler designed to handle scaladocs, Java projects & get `scalac` outputs. * * @param defaultJavaCommand * the default `java` command to be used * @param defaultJavaOptions * the default jvm options to be used with the `java` command * @param scaladoc * a flag for setting whether this compiler will handle scaladocs */ final case class SimpleScalaCompiler( defaultJavaCommand: String, defaultJavaOptions: Seq[String], scaladoc: Boolean ) extends ScalaCompiler { def jvmVersion: Option[Positioned[Int]] = None // ??? TODO def prepareProject( project: Project, logger: Logger ): Boolean = // no incremental compilation, always compiling everything every time true override def usesClassDir: Boolean = !scaladoc /** Run a synthetic (created in runtime) `scalac` as a JVM process with the specified parameters * * @param mainClass * the main class of the synthetic Scala compiler * @param javaHomeOpt * Java home path (optional) * @param javacOptions * options to be passed for the Java compiler * @param scalacOptions * options to be passed for the Scala compiler * @param classPath * class path to be passed to `scalac` * @param compilerClassPath * class path for the Scala compiler itself * @param sources * sources to be passed when running `scalac` (optional) * @param outputDir * output directory for the compiler (optional) * @param argsFileDir * output directory for the args file (optional) * @param cwd * working directory for running the compiler * @param logger * logger * @return * compiler process exit code */ private def runScalacLike( mainClass: String, javaHomeOpt: Option[os.Path], javacOptions: Seq[String], scalacOptions: Seq[String], classPath: Seq[os.Path], compilerClassPath: Seq[os.Path], sources: Seq[String], outputDir: Option[os.Path], argsFilePath: Option[os.Path], cwd: os.Path, logger: Logger ): Int = { outputDir.foreach(os.makeDir.all(_)) // initially adapted from https://github.com/VirtusLab/scala-cli/pull/103/files#diff-d13a7e6d602b8f84d9177e3138487872f0341d006accfe425886a561f029a9c3R120 and around val outputDirArgs = outputDir.map(od => Seq("-d", od.toString())).getOrElse(Nil) val classPathArgs = if (classPath.nonEmpty) Seq("-cp", classPath.map(_.toString).mkString(File.pathSeparator)) else Nil val args = { val freeArgs = scalacOptions ++ outputDirArgs ++ classPathArgs ++ sources if (freeArgs.size > Constants.maxScalacArgumentsCount) argsFilePath.fold(freeArgs) { path => os.write(path, freeArgs.mkString(System.lineSeparator())) Seq(s"@$path") } else freeArgs } val javaCommand = javaHomeOpt.map(SimpleJavaCompiler.javaCommand(_)).getOrElse(defaultJavaCommand) val javaOptions = defaultJavaOptions ++ scalacOptions .filter(_.startsWith("-J")) .map(_.stripPrefix("-J")) ++ javacOptions .filter(_.startsWith("-J")) .map(_.stripPrefix("-J")) Runner.runJvm( javaCommand, javaOptions, compilerClassPath, mainClass, args, logger, cwd = Some(cwd) ).waitFor() } /** Run a synthetic (created in runtime) `scalac` as a JVM process for a given * [[scala.build.Project]] * * @param project * project to be compiled * @param mainClass * the main class of the synthetic Scala compiler * @param outputDir * the scala compiler output directory * @param logger * logger * @return * true if the process returned no errors, false otherwise */ private def runScalacLikeForProject( project: Project, mainClass: String, outputDir: os.Path, logger: Logger ): Boolean = { val res = runScalacLike( mainClass = mainClass, javaHomeOpt = project.javaHomeOpt, javacOptions = project.javacOptions, scalacOptions = project.scalaCompiler.map(_.scalacOptions).getOrElse(Nil), classPath = project.classPath, compilerClassPath = project.scalaCompiler.map(_.compilerClassPath).getOrElse(Nil), sources = project.sources.map(_.toString), outputDir = Some(outputDir), argsFilePath = Some(project.argsFilePath), cwd = project.workspace, logger = logger ) res == 0 } /** Run a synthetic (created in runtime) `scalac` as a JVM process with minimal parameters. (i.e. * to print `scalac` help) * * @param scalaVersion * Scala version for which `scalac` is to be created * @param javaHomeOpt * Java home path (optional) * @param javacOptions * options to be passed for the Java compiler * @param scalacOptions * options to be passed for the Scala compiler * @param fullClassPath * classpath to be passed to the compiler (optional) * @param compilerClassPath * classpath of the compiler itself * @param logger * logger * @return * compiler process exit code */ def runSimpleScalacLike( scalaVersion: String, javaHomeOpt: Option[os.Path], javacOptions: Seq[String], scalacOptions: Seq[String], fullClassPath: Seq[os.Path], compilerClassPath: Seq[os.Path], logger: Logger ): Int = compilerMainClass(scalaVersion) match { case Some(mainClass) => runScalacLike( mainClass = mainClass, javaHomeOpt = javaHomeOpt, javacOptions = javacOptions, scalacOptions = scalacOptions, classPath = fullClassPath, compilerClassPath = compilerClassPath, sources = Nil, outputDir = None, argsFilePath = None, cwd = os.pwd, logger = logger ) case _ => 1 } private def compilerMainClass(scalaVersion: String): Option[String] = if (scalaVersion.startsWith("2.")) Some { if (scaladoc) "scala.tools.nsc.ScalaDoc" else "scala.tools.nsc.Main" } else if (scaladoc) None else Some("dotty.tools.dotc.Main") def compile( project: Project, logger: Logger ): Boolean = if (project.sources.isEmpty) true else project.scalaCompiler match { case Some(compiler) => val isScala2 = compiler.scalaVersion.startsWith("2.") compilerMainClass(compiler.scalaVersion).forall { mainClass => val outputDir = if (isScala2 && scaladoc) project.scaladocDir else project.classesDir runScalacLikeForProject(project, mainClass, outputDir, logger) } case None => scaladoc || SimpleJavaCompiler(defaultJavaCommand, defaultJavaOptions).compile(project, logger) } def shutdown(): Unit = () } ================================================ FILE: modules/build/src/main/scala/scala/build/compiler/SimpleScalaCompilerMaker.scala ================================================ package scala.build.compiler import ch.epfl.scala.bsp4j.BuildClient import scala.build.Logger import scala.build.errors.BuildException import scala.build.options.BuildOptions final case class SimpleScalaCompilerMaker( defaultJavaCommand: String, defaultJavaOptions: Seq[String], scaladoc: Boolean = false ) extends ScalaCompilerMaker { def create( workspace: os.Path, classesDir: os.Path, buildClient: BuildClient, logger: Logger, buildOptions: BuildOptions ): Either[BuildException, ScalaCompiler] = Right(SimpleScalaCompiler(defaultJavaCommand, defaultJavaOptions, scaladoc)) } ================================================ FILE: modules/build/src/main/scala/scala/build/input/Element.scala ================================================ package scala.build.input import scala.build.preprocessing.ScopePath import scala.util.matching.Regex sealed abstract class Element extends Product with Serializable sealed trait SingleElement extends Element sealed trait AnyScript extends Element sealed abstract class OnDisk extends Element { def path: os.Path } sealed abstract class Virtual extends SingleElement { def content: Array[Byte] def source: String def subPath: os.SubPath = { val idx = source.lastIndexOf('/') os.sub / source.drop(idx + 1) } def scopePath: ScopePath = ScopePath(Left(source), subPath) } object Virtual { val urlPathWithQueryParamsRegex = "https?://.*/([^/^?]+)(/?.*)?$".r def apply(path: String, content: Array[Byte]): Virtual = { val filename = path match { case urlPathWithQueryParamsRegex(name, _) => name case _ => path.split("/").last } val wrapperPath = os.sub / filename if filename.endsWith(".scala") then VirtualScalaFile(content, path) else if filename.endsWith(".java") then VirtualJavaFile(content, path) else if filename.endsWith(".sc") then VirtualScript(content, path, wrapperPath) else if filename.endsWith(".md") then VirtualMarkdownFile(content, path, wrapperPath) else VirtualData(content, path) } } sealed abstract class VirtualSourceFile extends Virtual { def isStdin: Boolean = source.startsWith("") def isSnippet: Boolean = source.startsWith("") protected def generatedSourceFileName(fileSuffix: String): String = if (isStdin) s"stdin$fileSuffix" else if (isSnippet) s"${source.stripPrefix("-")}$fileSuffix" else s"virtual$fileSuffix" } sealed trait SingleFile extends OnDisk with SingleElement sealed trait SourceFile extends SingleFile { def subPath: os.SubPath } sealed trait Compiled extends Element sealed trait AnyScalaFile extends Compiled sealed trait AnyJavaFile extends Compiled sealed trait AnyMarkdownFile extends Compiled sealed trait ScalaFile extends AnyScalaFile { def base: os.Path def subPath: os.SubPath def path: os.Path = base / subPath } final case class Script(base: os.Path, subPath: os.SubPath, inputArg: Option[String]) extends OnDisk with SourceFile with AnyScalaFile with AnyScript { lazy val path: os.Path = base / subPath } final case class SourceScalaFile(base: os.Path, subPath: os.SubPath) extends OnDisk with SourceFile with ScalaFile final case class ProjectScalaFile(base: os.Path, subPath: os.SubPath) extends OnDisk with SourceFile with ScalaFile final case class JavaFile(base: os.Path, subPath: os.SubPath) extends OnDisk with SourceFile with AnyJavaFile { lazy val path: os.Path = base / subPath } final case class JarFile(base: os.Path, subPath: os.SubPath) extends OnDisk with SourceFile { lazy val path: os.Path = base / subPath } final case class CFile(base: os.Path, subPath: os.SubPath) extends OnDisk with SourceFile with Compiled { lazy val path: os.Path = base / subPath } final case class MarkdownFile(base: os.Path, subPath: os.SubPath) extends OnDisk with SourceFile with AnyMarkdownFile { lazy val path: os.Path = base / subPath } final case class SbtFile(base: os.Path, subPath: os.SubPath) extends OnDisk with SourceFile { lazy val path: os.Path = base / subPath } final case class Directory(path: os.Path) extends OnDisk with Compiled final case class ResourceDirectory(path: os.Path) extends OnDisk final case class VirtualScript(content: Array[Byte], source: String, wrapperPath: os.SubPath) extends VirtualSourceFile with AnyScalaFile with AnyScript object VirtualScript { val VirtualScriptNameRegex: Regex = "(^stdin$|^snippet\\d*$)".r } final case class VirtualScalaFile(content: Array[Byte], source: String) extends VirtualSourceFile with AnyScalaFile { def generatedSourceFileName: String = generatedSourceFileName(".scala") } final case class VirtualJavaFile(content: Array[Byte], source: String) extends VirtualSourceFile with AnyJavaFile { def generatedSourceFileName: String = generatedSourceFileName(".java") } final case class VirtualMarkdownFile( content: Array[Byte], override val source: String, wrapperPath: os.SubPath ) extends VirtualSourceFile with AnyMarkdownFile final case class VirtualData(content: Array[Byte], source: String) extends Virtual ================================================ FILE: modules/build/src/main/scala/scala/build/input/ElementsUtils.scala ================================================ package scala.build.input import java.math.BigInteger import java.nio.charset.StandardCharsets import java.security.MessageDigest import scala.build.Directories import scala.build.internal.Constants object ElementsUtils { extension (p: os.Path) { def hasShebang: Boolean = os.isFile(p) && !p.toString.startsWith("/dev/fd/") && String(os.read.bytes(p, offset = 0, count = 2)) == "#!" def isScript: Boolean = p.ext == "sc" || (p.hasShebang && p.ext.isEmpty) } extension (d: Directory) { def singleFilesFromDirectory(enableMarkdown: Boolean): Seq[SingleFile] = { import Ordering.Implicits.seqOrdering os.walk.stream(d.path, skip = _.last.startsWith(".")) .filter(os.isFile(_)) .collect { case p if p.last.endsWith(".java") => JavaFile(d.path, p.subRelativeTo(d.path)) case p if p.last == Constants.projectFileName => ProjectScalaFile(d.path, p.subRelativeTo(d.path)) case p if p.last.endsWith(".scala") => SourceScalaFile(d.path, p.subRelativeTo(d.path)) case p if p.last.endsWith(".c") || p.last.endsWith(".h") => CFile(d.path, p.subRelativeTo(d.path)) case p if p.last.endsWith(".md") && enableMarkdown => MarkdownFile(d.path, p.subRelativeTo(d.path)) case p if p.last.endsWith(".sc") => // TODO: hasShebang test without consuming 1st 2 bytes of Stream Script(d.path, p.subRelativeTo(d.path), None) case p if p.last.endsWith(".sbt") => SbtFile(d.path, p.subRelativeTo(d.path)) } .toVector .sortBy(_.subPath.segments) } def configFile: Seq[ProjectScalaFile] = if (os.exists(d.path / Constants.projectFileName)) Seq(ProjectScalaFile(d.path, os.sub / Constants.projectFileName)) else Nil } extension (elements: Seq[Element]) { def projectSettingsFiles: Seq[ProjectScalaFile] = elements.flatMap { case f: ProjectScalaFile => Seq(f) case d: Directory => d.configFile case _ => Nil }.distinct def inputsHash: String = { def bytes(s: String): Array[Byte] = s.getBytes(StandardCharsets.UTF_8) val it = elements.iterator.flatMap { case elem: OnDisk => val prefix = elem match { case _: Directory => "dir:" case _: ResourceDirectory => "resource-dir:" case _: JavaFile => "java:" case _: ProjectScalaFile => "config:" case _: SourceScalaFile => "scala:" case _: CFile => "c:" case _: Script => "sc:" case _: MarkdownFile => "md:" case _: JarFile => "jar:" case _: SbtFile => "sbt:" } Iterator(prefix, elem.path.toString, "\n").map(bytes) case v: Virtual => Iterator(bytes("virtual:"), v.content, bytes(v.source), bytes("\n")) } val md = MessageDigest.getInstance("SHA-1") it.foreach(md.update) val digest = md.digest() val calculatedSum = new BigInteger(1, digest) String.format(s"%040x", calculatedSum).take(10) } def homeWorkspace(directories: Directories): os.Path = { val hash0 = elements.inputsHash val dir = directories.virtualProjectsDir / hash0.take(2) / s"project-${hash0.drop(2)}" os.makeDir.all(dir) dir } } } ================================================ FILE: modules/build/src/main/scala/scala/build/input/Inputs.scala ================================================ package scala.build.input import java.io.{ByteArrayInputStream, File} import java.math.BigInteger import java.nio.charset.StandardCharsets import java.security.MessageDigest import scala.annotation.tailrec import scala.build.Directories import scala.build.errors.{BuildException, InputsException, WorkspaceError} import scala.build.input.ElementsUtils.* import scala.build.internal.Constants import scala.build.internal.zip.WrappedZipInputStream import scala.build.options.{BuildOptions, Scope} import scala.build.preprocessing.SheBang.isShebangScript import scala.util.matching.Regex import scala.util.{Properties, Try} final case class Inputs( elements: Seq[Element], defaultMainClassElement: Option[Script], workspace: os.Path, baseProjectName: String, mayAppendHash: Boolean, workspaceOrigin: Option[WorkspaceOrigin], enableMarkdown: Boolean, allowRestrictedFeatures: Boolean ) { def isEmpty: Boolean = elements.isEmpty def singleFiles(): Seq[SingleFile] = elements.flatMap { case f: SingleFile => Seq(f) case d: Directory => d.singleFilesFromDirectory(enableMarkdown) case _: ResourceDirectory => Nil case _: Virtual => Nil } def sourceFiles(): Seq[SourceFile] = singleFiles().collect { case f: SourceFile => f } def flattened(): Seq[SingleElement] = elements.flatMap { case f: SingleFile => Seq(f) case d: Directory => d.singleFilesFromDirectory(enableMarkdown) case _: ResourceDirectory => Nil case v: Virtual => Seq(v) } private lazy val inputsHash: String = elements.inputsHash lazy val projectName: String = { val needsSuffix = mayAppendHash && (elements match { case Seq(d: Directory) => d.path != workspace case _ => true }) if needsSuffix then s"$baseProjectName-$inputsHash" else baseProjectName } def scopeProjectName(scope: Scope): String = if scope == Scope.Main then projectName else s"$projectName-${scope.name}" def add(extraElements: Seq[Element]): Inputs = if elements.isEmpty then this else copy(elements = (elements ++ extraElements).distinct) def withElements(elements: Seq[Element]): Inputs = copy(elements = elements) def generatedSrcRoot(scope: Scope): os.Path = workspace / Constants.workspaceDirName / projectName / "src_generated" / scope.name private def inHomeDir(directories: Directories): Inputs = copy( workspace = elements.homeWorkspace(directories), mayAppendHash = false, workspaceOrigin = Some(WorkspaceOrigin.HomeDir) ) def avoid(forbidden: Seq[os.Path], directories: Directories): Inputs = if forbidden.exists(workspace.startsWith) then inHomeDir(directories) else this def checkAttributes(directories: Directories): Inputs = { @tailrec def existingParent(p: os.Path): Option[os.Path] = if (os.exists(p)) Some(p) else if (p.segmentCount <= 0) None else existingParent(p / os.up) def reallyOwnedByUser(p: os.Path): Boolean = if (Properties.isWin) p.toIO.canWrite // Wondering if there's a better way to do that… else { val maybeUserHome = Try(os.owner(os.home)).toOption maybeUserHome.exists(_ == os.owner(p)) && p.toIO.canWrite } val canWrite = existingParent(workspace).exists(reallyOwnedByUser) if canWrite then this else inHomeDir(directories) } def sourceHash(): String = { def bytes(s: String): Array[Byte] = s.getBytes(StandardCharsets.UTF_8) val it = elements.iterator.flatMap { case elem: OnDisk => val content = elem match { case dirInput: Directory => Seq("dir:") ++ dirInput.singleFilesFromDirectory(enableMarkdown) .map(file => s"${file.path}:" + os.read(file.path)) case _: ResourceDirectory => Nil case _: SbtFile => Nil case _ => Seq(os.read(elem.path)) } (Iterator(elem.path.toString) ++ content.iterator ++ Iterator("\n")).map(bytes) case v: Virtual => Iterator(v.content, bytes("\n")) } val md = MessageDigest.getInstance("SHA-1") it.foreach(md.update) val digest = md.digest() val calculatedSum = new BigInteger(1, digest) String.format(s"%040x", calculatedSum) } def nativeWorkDir: os.Path = workspace / Constants.workspaceDirName / projectName / "native" def nativeImageWorkDir: os.Path = workspace / Constants.workspaceDirName / projectName / "native-image" def libraryJarWorkDir: os.Path = workspace / Constants.workspaceDirName / projectName / "jar" def docJarWorkDir: os.Path = workspace / Constants.workspaceDirName / projectName / "doc" } object Inputs { private def forValidatedElems( validElems: Seq[Element], workspace: os.Path, needsHash: Boolean, workspaceOrigin: WorkspaceOrigin, enableMarkdown: Boolean, allowRestrictedFeatures: Boolean, extraClasspathWasPassed: Boolean ): Inputs = { assert(extraClasspathWasPassed || validElems.nonEmpty) val allDirs = validElems.collect { case d: Directory => d.path } val updatedElems = validElems.filter { case f: SourceFile => val isInDir = allDirs.exists(f.path.relativeTo(_).ups == 0) !isInDir case _: Directory => true case _: ResourceDirectory => true case _: Virtual => true } // only on-disk scripts need a main class override val defaultMainClassElemOpt = validElems.collectFirst { case script: Script => script } Inputs( updatedElems, defaultMainClassElemOpt, workspace, baseName(workspace), mayAppendHash = needsHash, workspaceOrigin = Some(workspaceOrigin), enableMarkdown = enableMarkdown, allowRestrictedFeatures = allowRestrictedFeatures ) } private val githubGistsArchiveRegex: Regex = s"""://gist\\.github\\.com/[^/]*?/[^/]*$$""".r private def resolveZipArchive(content: Array[Byte], enableMarkdown: Boolean): Seq[Element] = { val zipInputStream = WrappedZipInputStream.create(new ByteArrayInputStream(content)) zipInputStream.entries().foldLeft(List.empty[Element]) { (acc, ent) => if (ent.isDirectory) acc else { val content = zipInputStream.readAllBytes() (Virtual(ent.getName, content) match { case _: AnyMarkdownFile if !enableMarkdown => None case e: Element => Some(e) }) map { element => element :: acc } getOrElse acc } } } def validateSnippets( scriptSnippetList: List[String] = List.empty, scalaSnippetList: List[String] = List.empty, javaSnippetList: List[String] = List.empty, markdownSnippetList: List[String] = List.empty ): Seq[Either[String, Seq[Element]]] = { def validateSnippet( snippetList: List[String], f: (Array[Byte], String) => Element ): Seq[Either[String, Seq[Element]]] = snippetList.zipWithIndex.map { case (snippet, index) => val snippetName: String = if (index > 0) s"snippet$index" else "snippet" if (snippet.nonEmpty) Right(Seq(f(snippet.getBytes(StandardCharsets.UTF_8), snippetName))) else Left(s"Empty snippet was passed: $snippetName") } Seq( validateSnippet( scriptSnippetList, (content, snippetName) => VirtualScript(content, snippetName, os.sub / s"$snippetName.sc") ), validateSnippet( scalaSnippetList, (content, snippetNameSuffix) => VirtualScalaFile(content, s"-scala-$snippetNameSuffix") ), validateSnippet( javaSnippetList, (content, snippetNameSuffix) => VirtualJavaFile(content, s"-java-$snippetNameSuffix") ), validateSnippet( markdownSnippetList, (content, snippetNameSuffix) => VirtualMarkdownFile( content, s"-markdown-$snippetNameSuffix", os.sub / s"$snippetNameSuffix.md" ) ) ).flatten } def validateArgs( args: Seq[String], cwd: os.Path, download: BuildOptions.Download, stdinOpt: => Option[Array[Byte]], acceptFds: Boolean, enableMarkdown: Boolean )(using programInvokeData: ScalaCliInvokeData): Seq[Either[String, Seq[Element]]] = args.zipWithIndex.map { case (arg, idx) => lazy val path = os.Path(arg, cwd) lazy val dir = path / os.up lazy val subPath = path.subRelativeTo(dir) lazy val stdinOpt0 = stdinOpt lazy val content = os.read.bytes(path) lazy val fullProgramCall = programInvokeData.progName + s"${ if programInvokeData.subCommand == SubCommand.Default then "" else s" ${programInvokeData.subCommandName}" }" val unrecognizedSourceErrorMsg = s"$arg: unrecognized source type (expected .scala or .sc extension, or a directory)." val missingShebangHeaderErrorMsg = s"""$unrecognizedSourceErrorMsg |If $arg is meant to be treated as a script, add a shebang header in its top line. | ${Console.BOLD}#!/usr/bin/env -S ${programInvokeData.progName} shebang${Console.RESET} |When a shebang header is provided, the script can then be run with the 'shebang' sub-command, even if no file extension is present. | ${Console.BOLD}${programInvokeData.progName} shebang $arg${Console.RESET}""".stripMargin if (arg == "-.scala" || arg == "_" || arg == "_.scala") && stdinOpt0.nonEmpty then Right(Seq(VirtualScalaFile(stdinOpt0.get, "-scala-file"))) else if (arg == "-.java" || arg == "_.java") && stdinOpt0.nonEmpty then Right(Seq(VirtualJavaFile(stdinOpt0.get, "-java-file"))) else if (arg == "-" || arg == "-.sc" || arg == "_.sc") && stdinOpt0.nonEmpty then Right(Seq(VirtualScript(stdinOpt0.get, "stdin", os.sub / "stdin.sc"))) else if (arg == "-.md" || arg == "_.md") && stdinOpt0.nonEmpty then Right(Seq(VirtualMarkdownFile( stdinOpt0.get, "-markdown-file", os.sub / "stdin.md" ))) else if arg.endsWith(".zip") && os.exists(path) then Right(resolveZipArchive(content, enableMarkdown)) else if arg.contains("://") then { val isGithubGist = githubGistsArchiveRegex.findFirstMatchIn(arg).nonEmpty val url = if isGithubGist then s"$arg/download" else arg download(url).map { urlContent => if isGithubGist then resolveZipArchive(urlContent, enableMarkdown) else List(Virtual(url, urlContent)) } } else if path.last == Constants.projectFileName then Right(Seq(ProjectScalaFile(dir, subPath))) else if os.isDir(path) then Right(Seq(Directory(path))) else if arg.endsWith(".sc") then Right(Seq(Script(dir, subPath, Some(arg)))) else if arg.endsWith(".scala") then Right(Seq(SourceScalaFile(dir, subPath))) else if arg.endsWith(".java") then Right(Seq(JavaFile(dir, subPath))) else if arg.endsWith(".jar") then Right(Seq(JarFile(dir, subPath))) else if arg.endsWith(".c") || arg.endsWith(".h") then Right(Seq(CFile(dir, subPath))) else if arg.endsWith(".sbt") then Right(Seq(SbtFile(dir, subPath))) else if arg.endsWith(".md") then Right(Seq(MarkdownFile(dir, subPath))) else if acceptFds && arg.startsWith("/dev/fd/") then Right(Seq(VirtualScript(content, arg, os.sub / s"input-${idx + 1}.sc"))) else if path.ext.isEmpty && os.exists(path) && path.isScript then Right(Seq(Script(dir, subPath, Some(arg)))) else if programInvokeData.subCommand == SubCommand.Shebang && os.exists(path) then if isShebangScript(String(content)) then Right(Seq(Script(dir, subPath, Some(arg)))) else Left( if programInvokeData.isShebangCapableShell then missingShebangHeaderErrorMsg else unrecognizedSourceErrorMsg ) else { val msg = if os.exists(path) then programInvokeData match { case ScalaCliInvokeData(progName, _, _, true) if isShebangScript(String(content)) => s"""$arg: scripts with no file extension should be run with the 'shebang' sub-command. | ${Console.BOLD}$progName shebang $arg${Console.RESET}""".stripMargin case ScalaCliInvokeData(_, _, _, true) => missingShebangHeaderErrorMsg case _ => unrecognizedSourceErrorMsg } else if programInvokeData.subCommand == SubCommand.Default && idx == 0 && arg.forall(_.isLetterOrDigit) then s"""$arg is not a ${programInvokeData.progName} sub-command and it is not a valid path to an input file or directory. |Try viewing the relevant help to see the list of available sub-commands and options. | ${Console.BOLD}${programInvokeData.progName} --help${Console.RESET}""".stripMargin else s"""$arg: input file not found |Try viewing the relevant help to see the list of available sub-commands and options. | ${Console.BOLD}$fullProgramCall --help${Console.RESET}""".stripMargin Left(msg) } } private def forNonEmptyArgs( args: Seq[String], cwd: os.Path, download: String => Either[String, Array[Byte]], stdinOpt: => Option[Array[Byte]], scriptSnippetList: List[String], scalaSnippetList: List[String], javaSnippetList: List[String], markdownSnippetList: List[String], acceptFds: Boolean, forcedWorkspace: Option[os.Path], enableMarkdown: Boolean, allowRestrictedFeatures: Boolean, extraClasspathWasPassed: Boolean )(using invokeData: ScalaCliInvokeData): Either[BuildException, Inputs] = { val validatedArgs: Seq[Either[String, Seq[Element]]] = validateArgs( args, cwd, download, stdinOpt, acceptFds, enableMarkdown ) val validatedSnippets: Seq[Either[String, Seq[Element]]] = validateSnippets(scriptSnippetList, scalaSnippetList, javaSnippetList, markdownSnippetList) val validatedArgsAndSnippets = validatedArgs ++ validatedSnippets val invalid = validatedArgsAndSnippets.collect { case Left(msg) => msg } if (invalid.isEmpty) { val validElems = validatedArgsAndSnippets.collect { case Right(elem) => elem }.flatten assert(extraClasspathWasPassed || validElems.nonEmpty) val (inferredWorkspace, inferredNeedsHash, workspaceOrigin) = { val settingsFiles = validElems.projectSettingsFiles val dirsAndFiles = validElems.collect { case d: Directory => d case f: SourceFile => f } settingsFiles.headOption.map { s => if (settingsFiles.length > 1) System.err.println( s"Warning: more than one ${Constants.projectFileName} file has been found. Setting ${s.base} as the project root directory for this run." ) (s.base, true, WorkspaceOrigin.SourcePaths) }.orElse { dirsAndFiles.collectFirst { case d: Directory => if (dirsAndFiles.length > 1) System.err.println( s"Warning: setting ${d.path} as the project root directory for this run." ) (d.path, true, WorkspaceOrigin.SourcePaths) case f: SourceFile => if (dirsAndFiles.length > 1) System.err.println( s"Warning: setting ${f.path / os.up} as the project root directory for this run." ) (f.path / os.up, true, WorkspaceOrigin.SourcePaths) } }.orElse { validElems.collectFirst { case _: Virtual => (os.temp.dir(), true, WorkspaceOrigin.VirtualForced) } }.getOrElse((os.pwd, true, WorkspaceOrigin.Forced)) } val (workspace, needsHash, workspaceOrigin0) = forcedWorkspace match { case None => (inferredWorkspace, inferredNeedsHash, workspaceOrigin) case Some(forcedWorkspace0) => val needsHash0 = forcedWorkspace0 != inferredWorkspace || inferredNeedsHash (forcedWorkspace0, needsHash0, WorkspaceOrigin.Forced) } if workspace.toString.contains(File.pathSeparator) then val prog = invokeData.invocationString val argsString = args.mkString(" ") Left(new WorkspaceError( s"""Invalid workspace path: ${Console.BOLD}$workspace${Console.RESET} |Workspace path cannot contain a ${Console.BOLD}${File.pathSeparator}${Console.RESET}. |Consider moving your project to a different path. |Alternatively, you can force your workspace with the '--workspace' option: | ${Console.BOLD}$prog --workspace $argsString${Console .RESET}""" .stripMargin )) else Right(forValidatedElems( validElems, workspace, needsHash, workspaceOrigin0, enableMarkdown, allowRestrictedFeatures, extraClasspathWasPassed )) } else Left(new InputsException(invalid.mkString(System.lineSeparator()))) } def apply( args: Seq[String], cwd: os.Path, defaultInputs: () => Option[Inputs] = () => None, download: BuildOptions.Download = BuildOptions.Download.notSupported, stdinOpt: => Option[Array[Byte]] = None, scriptSnippetList: List[String] = List.empty, scalaSnippetList: List[String] = List.empty, javaSnippetList: List[String] = List.empty, markdownSnippetList: List[String] = List.empty, acceptFds: Boolean = false, forcedWorkspace: Option[os.Path] = None, enableMarkdown: Boolean = false, allowRestrictedFeatures: Boolean, extraClasspathWasPassed: Boolean )(using ScalaCliInvokeData): Either[BuildException, Inputs] = if ( args.isEmpty && scriptSnippetList.isEmpty && scalaSnippetList.isEmpty && javaSnippetList.isEmpty && markdownSnippetList.isEmpty && !extraClasspathWasPassed ) defaultInputs().toRight(new InputsException( "No inputs provided (expected files with .scala, .sc, .java or .md extensions, and / or directories)." )) else forNonEmptyArgs( args, cwd, download, stdinOpt, scriptSnippetList, scalaSnippetList, javaSnippetList, markdownSnippetList, acceptFds, forcedWorkspace, enableMarkdown, allowRestrictedFeatures, extraClasspathWasPassed ) def default(): Option[Inputs] = None def empty(workspace: os.Path, enableMarkdown: Boolean): Inputs = Inputs( elements = Nil, defaultMainClassElement = None, workspace = workspace, baseProjectName = baseName(workspace), mayAppendHash = true, workspaceOrigin = None, enableMarkdown = enableMarkdown, allowRestrictedFeatures = false ) def empty(projectName: String): Inputs = Inputs(Nil, None, os.pwd, projectName, false, None, true, false) def baseName(p: os.Path) = if (p == os.root || p.lastOpt.isEmpty) "" else p.baseName } ================================================ FILE: modules/build/src/main/scala/scala/build/input/ScalaCliInvokeData.scala ================================================ package scala.build.input /** Stores information about how the program has been evoked * * @param progName * the actual Scala CLI program name which was run * @param subCommandName * the name of the sub-command that was invoked by user * @param subCommand * the type of the sub-command that was invoked by user * @param isShebangCapableShell * does the host shell support shebang headers */ case class ScalaCliInvokeData( progName: String, subCommandName: String, subCommand: SubCommand, isShebangCapableShell: Boolean ) { /** [[progName]] with [[subCommandName]] if any */ def invocationString: String = subCommand match case SubCommand.Default => progName case _ => s"$progName $subCommandName" } object ScalaCliInvokeData { def dummy: ScalaCliInvokeData = ScalaCliInvokeData("", "", SubCommand.Other, false) } enum SubCommand: case Default extends SubCommand case Shebang extends SubCommand case Other extends SubCommand ================================================ FILE: modules/build/src/main/scala/scala/build/input/WorkspaceOrigin.scala ================================================ package scala.build.input sealed abstract class WorkspaceOrigin extends Product with Serializable object WorkspaceOrigin { case object Forced extends WorkspaceOrigin case object SourcePaths extends WorkspaceOrigin case object ResourcePaths extends WorkspaceOrigin case object HomeDir extends WorkspaceOrigin case object VirtualForced extends WorkspaceOrigin } ================================================ FILE: modules/build/src/main/scala/scala/build/internal/AmmUtil.scala ================================================ package scala.build.internal // adapted from https://github.com/com-lihaoyi/Ammonite/blob/9be39debc367abad5f5541ef58f4b986b2a8d045/amm/util/src/main/scala/ammonite/util/Util.scala object AmmUtil { val upPathSegment = "^" def pathToPackageWrapper(relPath0: os.SubPath): (Seq[Name], Name) = { val relPath = relPath0 / os.up val fileName = relPath0.last val pkg = relPath.segments.map(Name(_)) val wrapper = fileName.lastIndexOf('.') match { case -1 => fileName case i => fileName.take(i) } (pkg, Name(wrapper)) } def encodeScalaSourcePath(path: Seq[Name]) = path.map(_.backticked).mkString(".") def normalizeNewlines(s: String): String = s.replace("\r", "") lazy val lineSeparator: String = "\n" } ================================================ FILE: modules/build/src/main/scala/scala/build/internal/AppCodeWrapper.scala ================================================ package scala.build.internal case class AppCodeWrapper(scalaVersion: String, log: String => Unit) extends CodeWrapper { override def mainClassObject(className: Name) = className def apply( code: String, pkgName: Seq[Name], indexedWrapperName: Name, extraCode: String, scriptPath: String ) = { val wrapperObjectName = indexedWrapperName.backticked val mainObject = WrapperUtils.mainObjectInScript(scalaVersion, code) val invokeMain = mainObject match case WrapperUtils.ScriptMainMethod.Exists(name) => s"\n$name.main(args)" case otherwise => otherwise.warningMessage.foreach(log) "" val packageDirective = if (pkgName.isEmpty) "" else s"package ${AmmUtil.encodeScalaSourcePath(pkgName)}" + "\n" val top = AmmUtil.normalizeNewlines( s"""$packageDirective | |object $wrapperObjectName extends App { |val scriptPath = \"\"\"$scriptPath\"\"\"$invokeMain |""".stripMargin ) val bottom = AmmUtil.normalizeNewlines( s""" |$extraCode |} |""".stripMargin ) (top, bottom) } } ================================================ FILE: modules/build/src/main/scala/scala/build/internal/ClassCodeWrapper.scala ================================================ package scala.build.internal /** Script code wrapper that solves problem of deadlocks when using threads. The code is placed in a * class instance constructor, the created object is kept in 'mainObjectCode'.script to support * running interconnected scripts using Scala CLI

Incompatible with Scala 2 - it uses * Scala 3 feature 'export'
Incompatible with native JS members - the wrapper is a class */ case class ClassCodeWrapper(scalaVersion: String, log: String => Unit) extends CodeWrapper { override def mainClassObject(className: Name): Name = Name(className.raw ++ "_sc") def apply( code: String, pkgName: Seq[Name], indexedWrapperName: Name, extraCode: String, scriptPath: String ) = { val mainObject = WrapperUtils.mainObjectInScript(scalaVersion, code) val mainInvocation = mainObject match case WrapperUtils.ScriptMainMethod.Exists(name) => s"script.$name.main(args)" case otherwise => otherwise.warningMessage.foreach(log) s"val _ = script.hashCode()" val name = mainClassObject(indexedWrapperName).backticked val wrapperClassName = scala.build.internal.Name(indexedWrapperName.raw ++ "$_").backticked val mainObjectCode = AmmUtil.normalizeNewlines(s"""|object $name { | private var args$$opt0 = Option.empty[Array[String]] | def args$$set(args: Array[String]): Unit = { | args$$opt0 = Some(args) | } | def args$$opt: Option[Array[String]] = args$$opt0 | def args$$: Array[String] = args$$opt.getOrElse { | sys.error("No arguments passed to this script") | } | | lazy val script = new $wrapperClassName | | def main(args: Array[String]): Unit = { | args$$set(args) | $mainInvocation // hashCode to clear scalac warning about pure expression in statement position | } |} | |export $name.script as `${indexedWrapperName.raw}` |""".stripMargin) val packageDirective = if (pkgName.isEmpty) "" else s"package ${AmmUtil.encodeScalaSourcePath(pkgName)}" + "\n" val top = AmmUtil.normalizeNewlines( s"""$packageDirective | |final class $wrapperClassName { |def args = $name.args$$ |def scriptPath = \"\"\"$scriptPath\"\"\" |""".stripMargin ) val bottom = AmmUtil.normalizeNewlines( s"""$extraCode |} | |$mainObjectCode |""".stripMargin ) (top, bottom) } } ================================================ FILE: modules/build/src/main/scala/scala/build/internal/JavaParserProxy.scala ================================================ package scala.build.internal import scala.build.errors.BuildException /** Helper to get class names from Java sources * * See [[JavaParserProxyJvm]] for the implementation that runs things in memory using * java-class-name from the class path, and [[JavaParserProxyBinary]] for the implementation that * downloads and runs a java-class-name binary. */ trait JavaParserProxy { /** Extracts the class name of a Java source, using the dotty Java parser. * * @param content * the Java source to extract a class name from * @return * either some class name (if one was found) or none (if none was found), or a * [[BuildException]] */ def className(content: Array[Byte]): Either[BuildException, Option[String]] } ================================================ FILE: modules/build/src/main/scala/scala/build/internal/JavaParserProxyBinary.scala ================================================ package scala.build.internal import coursier.cache.ArchiveCache import coursier.util.Task import dependency.* import java.util.function.Supplier import scala.build.EitherCps.{either, value} import scala.build.Logger import scala.build.errors.BuildException import scala.util.Properties /** Downloads and runs java-class-name as an external binary. */ class JavaParserProxyBinary( archiveCache: ArchiveCache[Task], javaClassNameVersionOpt: Option[String], logger: Logger, javaCommand: () => String ) extends JavaParserProxy { /** For internal use only * * Passing archiveCache as an Object, to work around issues with higher-kind type params from * Java code. */ def this( archiveCache: Object, logger: Logger, javaClassNameVersionOpt: Option[String], javaCommand0: Supplier[String] ) = this( archiveCache.asInstanceOf[ArchiveCache[Task]], javaClassNameVersionOpt, logger, () => javaCommand0.get() ) def className(content: Array[Byte]): Either[BuildException, Option[String]] = either { val platformSuffix = FetchExternalBinary.platformSuffix() val version = javaClassNameVersionOpt.getOrElse(Constants.javaClassNameVersion) val (tag, changing) = if (version == "latest") ("nightly", true) else ("v" + version, false) val ext = if (Properties.isWin) ".zip" else ".gz" val url = s"https://github.com/VirtusLab/java-class-name/releases/download/$tag/java-class-name-$platformSuffix$ext" val params = ExternalBinaryParams( url, changing, "java-class-name", Seq( dep"${Constants.javaClassNameOrganization}:${Constants.javaClassNameName}:${Constants.javaClassNameVersion}" ), "scala.cli.javaclassname.JavaClassName" // FIXME I'd rather not hardcode that, but automatic detection is cumbersome to setup… ) val binary = value(FetchExternalBinary.fetch(params, archiveCache, logger, javaCommand)) val source = os.temp(content, suffix = ".java", perms = if (Properties.isWin) null else "rw-------") val command = binary.command val output = try { logger.debug(s"Running $command $source") val res = os.proc(command, source).call() res.out.trim() } finally os.remove(source) if (output.isEmpty) None else Some(output) } } ================================================ FILE: modules/build/src/main/scala/scala/build/internal/JavaParserProxyJvm.scala ================================================ package scala.build.internal import scala.build.errors.BuildException import scala.cli.javaclassname.JavaParser /** A [[JavaParserProxy]] that relies on java-class-name in the class path, rather than downloading * it and running it as an external binary. * * Should be used from Scala CLI when it's run on the JVM. */ class JavaParserProxyJvm extends JavaParserProxy { override def className(content: Array[Byte]): Either[BuildException, Option[String]] = Right(JavaParser.parseRootPublicClassName(content)) } ================================================ FILE: modules/build/src/main/scala/scala/build/internal/JavaParserProxyMaker.scala ================================================ package scala.build.internal import java.util.function.Supplier import scala.annotation.unused import scala.build.Logger /** On the JVM, provides [[JavaParserProxyJvm]] as [[JavaParserProxy]] instance. * * From native launchers, [[JavaParserProxyMakerSubst]] takes over this, and gives * [[JavaParserProxyBinary]] instead. * * That way, no reference to [[JavaParserProxyJvm]] remains in the native call graph, and that * class and those it pulls (the java-class-name classes, which includes parts of the dotty parser) * are not embedded the native launcher. * * Note that this is a class and not an object, to make it easier to write substitutions for that * in Java. */ class JavaParserProxyMaker { def get( @unused archiveCache: Object, // Actually a ArchiveCache[Task], but having issues with the higher-kind type param from Java… @unused javaClassNameVersionOpt: Option[String], @unused logger: Logger, @unused javaCommand: Supplier[String] ): JavaParserProxy = new JavaParserProxyJvm } ================================================ FILE: modules/build/src/main/scala/scala/build/internal/MainClass.scala ================================================ package scala.build.internal import org.objectweb.asm import org.objectweb.asm.ClassReader import java.io.{ByteArrayInputStream, InputStream} import java.nio.file.NoSuchFileException import java.util.jar.{Attributes, JarFile} import scala.build.internal.zip.WrappedZipInputStream import scala.build.{Logger, retry} object MainClass { private def stringArrayDescriptor = "([Ljava/lang/String;)V" private class MainMethodChecker extends asm.ClassVisitor(asm.Opcodes.ASM9) { private var foundMainClass = false private var nameOpt = Option.empty[String] def found: Boolean = foundMainClass override def visit( version: Int, access: Int, name: String, signature: String, superName: String, interfaces: Array[String] ): Unit = { nameOpt = Some(name.replace('/', '.').replace('\\', '.')) } override def visitMethod( access: Int, name: String, descriptor: String, signature: String, exceptions: Array[String] ): asm.MethodVisitor = { def isStatic = (access & asm.Opcodes.ACC_STATIC) != 0 if (name == "main" && descriptor == stringArrayDescriptor && isStatic) foundMainClass = true null } def mainClassOpt: Option[String] = if (foundMainClass) nameOpt else None } private def findInClass(path: os.Path, logger: Logger): Iterator[String] = try { val is = retry()(logger)(os.read.inputStream(path)) findInClass(is, logger) } catch { case e: NoSuchFileException => e.getStackTrace.foreach(ste => logger.debug(ste.toString)) logger.log(s"Class file $path not found: $e") logger.log("Are you trying to run too many builds at once? Trying to recover...") Iterator.empty } private def findInClass(is: InputStream, logger: Logger): Iterator[String] = try retry()(logger) { val reader = new ClassReader(is) val checker = new MainMethodChecker reader.accept(checker, 0) checker.mainClassOpt.iterator } catch { case e: ArrayIndexOutOfBoundsException => e.getStackTrace.foreach(ste => logger.debug(ste.toString)) logger.log(s"Class input stream could not be created: $e") logger.log("Are you trying to run too many builds at once? Trying to recover...") Iterator.empty } finally is.close() private def findInJar(path: os.Path, logger: Logger): Iterator[String] = try retry()(logger) { val content = os.read.bytes(path) val jarInputStream = WrappedZipInputStream.create(new ByteArrayInputStream(content)) jarInputStream.entries().flatMap(ent => if !ent.isDirectory && ent.getName.endsWith(".class") then { val content = jarInputStream.readAllBytes() val inputStream = new ByteArrayInputStream(content) findInClass(inputStream, logger) } else Iterator.empty ) } catch { case e: NoSuchFileException => logger.debugStackTrace(e) logger.log(s"JAR file $path not found: $e, trying to recover...") logger.log("Are you trying to run too many builds at once? Trying to recover...") Iterator.empty } def findInDependency(jar: os.Path): Option[String] = jar match { case jar if os.isFile(jar) && jar.last.endsWith(".jar") => for { manifest <- Option(new JarFile(jar.toIO).getManifest) mainAttributes <- Option(manifest.getMainAttributes) mainClass: String <- Option(mainAttributes.getValue(Attributes.Name.MAIN_CLASS)) } yield mainClass case _ => None } def find(output: os.Path, logger: Logger): Seq[String] = output match { case o if os.isFile(o) && o.last.endsWith(".class") => findInClass(o, logger).toVector case o if os.isFile(o) && o.last.endsWith(".jar") => findInJar(o, logger).toVector case o if os.isDir(o) => os.walk(o) .iterator .filter(os.isFile) .flatMap { case classFilePath if classFilePath.last.endsWith(".class") => findInClass(classFilePath, logger) case _ => Iterator.empty } .toVector case _ => Vector.empty } } ================================================ FILE: modules/build/src/main/scala/scala/build/internal/ManifestJar.scala ================================================ package scala.build.internal import java.io.OutputStream object ManifestJar { /** Creates a manifest JAR, in a temporary directory or in the passed scratched directory * * @param classPath * Entries that should be put in the manifest class path * @param wrongSimplePaths * Write paths slightly differently in manifest, so that tools such as native-image accept them * (but the manifest JAR can't be passed to 'java -cp' any more on Windows) * @param scratchDirOpt * an optional scratch directory to write the manifest JAR under */ def create( classPath: Seq[os.Path], wrongSimplePaths: Boolean = false, scratchDirOpt: Option[os.Path] = None ): os.Path = { import java.util.jar._ val manifest = new Manifest val attributes = manifest.getMainAttributes attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0") attributes.put( Attributes.Name.CLASS_PATH, if (wrongSimplePaths) // For tools, such as native-image, that don't correctly handle paths in manifests… classPath.map(_.toString).mkString(" ") else // Paths are encoded this weird way in manifest JARs. This matters on Windows in particular, // where paths like "C:\…" don't work fine. classPath.map(_.toNIO.toUri.getRawPath).mkString(" ") ) val jarFile = scratchDirOpt match { case Some(scratchDir) => os.makeDir.all(scratchDir) os.temp(dir = scratchDir, prefix = "classpathJar", suffix = ".jar", deleteOnExit = false) case None => os.temp(prefix = "classpathJar", suffix = ".jar") } var os0: OutputStream = null var jos: JarOutputStream = null try { os0 = os.write.outputStream(jarFile) jos = new JarOutputStream(os0, manifest) } finally { if (jos != null) jos.close() if (os0 != null) os0.close() } jarFile } /** Runs a block of code using a manifest JAR. * * See [[create]] for details about the parameters. */ def maybeWithManifestClassPath[T]( createManifest: Boolean, classPath: Seq[os.Path], wrongSimplePathsInManifest: Boolean = false )( f: Seq[os.Path] => T ): T = if (createManifest) { var toDeleteOpt = Option.empty[os.Path] try { val manifestJar = create(classPath, wrongSimplePaths = wrongSimplePathsInManifest) toDeleteOpt = Some(manifestJar) f(Seq(manifestJar)) } finally for (toDelete <- toDeleteOpt) os.remove(toDelete) } else f(classPath) } ================================================ FILE: modules/build/src/main/scala/scala/build/internal/ObjectCodeWrapper.scala ================================================ package scala.build.internal /** Script code wrapper compatible with Scala 2 and JS native members

When using Scala 3 * or/and not using JS native prefer [[ClassCodeWrapper]], since it prevents deadlocks when running * threads from script */ case class ObjectCodeWrapper(scalaVersion: String, log: String => Unit) extends CodeWrapper { override def mainClassObject(className: Name): Name = Name(className.raw ++ "_sc") def apply( code: String, pkgName: Seq[Name], indexedWrapperName: Name, extraCode: String, scriptPath: String ) = { val mainObject = WrapperUtils.mainObjectInScript(scalaVersion, code) val name = mainClassObject(indexedWrapperName).backticked val aliasedWrapperName = name + "$$alias" val realScript = if (name == "main_sc") s"$aliasedWrapperName.alias" // https://github.com/VirtusLab/scala-cli/issues/314 else s"${indexedWrapperName.backticked}" val funHashCodeMethod = mainObject match case WrapperUtils.ScriptMainMethod.Exists(name) => s"$realScript.$name.main(args)" case otherwise => otherwise.warningMessage.foreach(log) s"val _ = $realScript.hashCode()" // We need to call hashCode (or any other method so compiler does not report a warning) val mainObjectCode = AmmUtil.normalizeNewlines(s"""|object $name { | private var args$$opt0 = Option.empty[Array[String]] | def args$$set(args: Array[String]): Unit = { | args$$opt0 = Some(args) | } | def args$$opt: Option[Array[String]] = args$$opt0 | def args$$: Array[String] = args$$opt.getOrElse { | sys.error("No arguments passed to this script") | } | def main(args: Array[String]): Unit = { | args$$set(args) | $funHashCodeMethod // hashCode to clear scalac warning about pure expression in statement position | } |} |""".stripMargin) val packageDirective = if (pkgName.isEmpty) "" else s"package ${AmmUtil.encodeScalaSourcePath(pkgName)}" + "\n" val aliasObject = if (name == "main_sc") s"""object $aliasedWrapperName { | val alias = ${indexedWrapperName.backticked} |}""".stripMargin else "" val top = AmmUtil.normalizeNewlines( s"""$packageDirective | |object ${indexedWrapperName.backticked} { |def args = $name.args$$ |def scriptPath = \"\"\"$scriptPath\"\"\" |""".stripMargin ) val bottom = AmmUtil.normalizeNewlines( s"""$extraCode |} |$aliasObject |$mainObjectCode |""".stripMargin ) (top, bottom) } } ================================================ FILE: modules/build/src/main/scala/scala/build/internal/Runner.scala ================================================ package scala.build.internal import coursier.jvm.Execve import org.scalajs.jsenv.jsdomnodejs.JSDOMNodeJSEnv import org.scalajs.jsenv.nodejs.NodeJSEnv import org.scalajs.jsenv.{Input, JSEnv, RunConfig} import org.scalajs.testing.adapter.TestAdapter as ScalaJsTestAdapter import sbt.testing.{Framework, Status} import java.io.File import java.nio.file.{Files, Path, Paths} import scala.build.EitherCps.{either, value} import scala.build.Logger import scala.build.errors.* import scala.build.internals.EnvVar import scala.build.testrunner.FrameworkUtils.* import scala.build.testrunner.{AsmTestRunner, Logger as TestRunnerLogger, TestRunner} import scala.scalanative.testinterface.adapter.TestAdapter as ScalaNativeTestAdapter import scala.util.{Failure, Properties, Success} object Runner { private def toTestRunnerLogger(logger: Logger): TestRunnerLogger = TestRunnerLogger(logger.verbosity) def maybeExec( commandName: String, command: Seq[String], logger: Logger, cwd: Option[os.Path] = None, extraEnv: Map[String, String] = Map.empty ): Process = run0( commandName, command, logger, allowExecve = true, cwd, extraEnv, inheritStreams = true ) def run( command: Seq[String], logger: Logger, cwd: Option[os.Path] = None, extraEnv: Map[String, String] = Map.empty, inheritStreams: Boolean = true ): Process = run0( "unused", command, logger, allowExecve = false, cwd, extraEnv, inheritStreams ) def run0( commandName: String, command: Seq[String], logger: Logger, allowExecve: Boolean, cwd: Option[os.Path], extraEnv: Map[String, String], inheritStreams: Boolean ): Process = { import logger.{log, debug} log( s"Running ${command.mkString(" ")}", " Running" + System.lineSeparator() + command.iterator.map(_ + System.lineSeparator()).mkString ) if (allowExecve && Execve.available()) { debug("execve available") for (dir <- cwd) Chdir.chdir(dir.toString) Execve.execve( findInPath(command.head).fold(command.head)(_.toString), commandName +: command.tail.toArray, (sys.env ++ extraEnv).toArray.sorted.map { case (k, v) => s"$k=$v" } ) sys.error("should not happen") } else { val b = new ProcessBuilder(command*) .inheritIO() if (!inheritStreams) { b.redirectInput(ProcessBuilder.Redirect.PIPE) b.redirectOutput(ProcessBuilder.Redirect.PIPE) } if (extraEnv.nonEmpty) { val env = b.environment() for ((k, v) <- extraEnv) env.put(k, v) } for (dir <- cwd) b.directory(dir.toIO) val process = b.start() process } } def envCommand(env: Map[String, String]): Seq[String] = env.toVector.sortBy(_._1).map { case (k, v) => s"$k=$v" } def jvmCommand( javaCommand: String, javaArgs: Seq[String], classPath: Seq[os.Path], mainClass: String, args: Seq[String], extraEnv: Map[String, String] = Map.empty, useManifest: Option[Boolean] = None, scratchDirOpt: Option[os.Path] = None ): Seq[String] = { def command(cp: Seq[os.Path]) = envCommand(extraEnv) ++ Seq(javaCommand) ++ javaArgs ++ Seq( "-cp", cp.iterator.map(_.toString).mkString(File.pathSeparator), mainClass ) ++ args val initialCommand = command(classPath) val useManifest0 = useManifest.getOrElse { Properties.isWin && { val commandLen = initialCommand.map(_.length).sum + (initialCommand.length - 1) // On Windows, total command lengths have this limit. Note that the same kind // of limit applies the environment, so that we can't sneak in info via env vars to // overcome the command length limit. // See https://devblogs.microsoft.com/oldnewthing/20031210-00/?p=41553 commandLen >= 32767 } } if (useManifest0) { val manifestJar = ManifestJar.create(classPath, scratchDirOpt = scratchDirOpt) command(Seq(manifestJar)) } else initialCommand } def runJvm( javaCommand: String, javaArgs: Seq[String], classPath: Seq[os.Path], mainClass: String, args: Seq[String], logger: Logger, allowExecve: Boolean = false, cwd: Option[os.Path] = None, extraEnv: Map[String, String] = Map.empty, useManifest: Option[Boolean] = None, scratchDirOpt: Option[os.Path] = None ): Process = { val command = jvmCommand( javaCommand, javaArgs, classPath, mainClass, args, Map.empty, useManifest, scratchDirOpt ) if (allowExecve) maybeExec("java", command, logger, cwd = cwd, extraEnv = extraEnv) else run(command, logger, cwd = cwd, extraEnv = extraEnv) } private def endsWithCaseInsensitive(s: String, suffix: String): Boolean = s.length >= suffix.length && s.regionMatches(true, s.length - suffix.length, suffix, 0, suffix.length) private def findInPath(app: String): Option[Path] = { val asIs = Paths.get(app) if (Paths.get(app).getNameCount >= 2) Some(asIs) else { def pathEntries = EnvVar.Misc.path.valueOpt .iterator .flatMap(_.split(File.pathSeparator).iterator) def pathSep = if (Properties.isWin) EnvVar.Misc.pathExt.valueOpt .iterator .flatMap(_.split(File.pathSeparator).iterator) else Iterator("") def matches = for { dir <- pathEntries ext <- pathSep app0 = if (endsWithCaseInsensitive(app, ext)) app else app + ext path = Paths.get(dir).resolve(app0) if Files.isExecutable(path) } yield path matches.take(1).toList.headOption } } def jsCommand( entrypoint: File, args: Seq[String], jsDom: Boolean = false ): Seq[String] = { val nodePath = findInPath("node").fold("node")(_.toString) val command = Seq(nodePath, entrypoint.getAbsolutePath) ++ args if (jsDom) // FIXME We'd need to replicate what JSDOMNodeJSEnv does under-the-hood to get the command in that case. // --command is mostly for debugging purposes, so I'm not sure it matters much here… sys.error("Cannot get command when JSDOM is enabled.") else "node" +: command.tail } def runJs( entrypoint: File, args: Seq[String], logger: Logger, allowExecve: Boolean = false, jsDom: Boolean = false, sourceMap: Boolean = false, esModule: Boolean = false ): Either[BuildException, Process] = either { val nodePath: String = value(findInPath("node") .map(_.toString) .toRight(NodeNotFoundError())) if !jsDom && allowExecve && Execve.available() then { val command = Seq(nodePath, entrypoint.getAbsolutePath) ++ args logger.log( s"Running ${command.mkString(" ")}", " Running" + System.lineSeparator() + command.iterator.map(_ + System.lineSeparator()).mkString ) logger.debug("execve available") Execve.execve( command.head, "node" +: command.tail.toArray, sys.env.toArray.sorted.map { case (k, v) => s"$k=$v" } ) sys.error("should not happen") } else { val nodeArgs = // Scala.js runs apps by piping JS to node. // If we need to pass arguments, we must first make the piped input explicit // with "-", and we pass the user's arguments after that. if args.isEmpty then Nil else "-" :: args.toList val envJs = if jsDom then new JSDOMNodeJSEnv( JSDOMNodeJSEnv.Config() .withExecutable(nodePath) .withArgs(nodeArgs) .withEnv(Map.empty) ) else new NodeJSEnv( NodeJSEnv.Config() .withExecutable(nodePath) .withArgs(nodeArgs) .withEnv(Map.empty) .withSourceMap(sourceMap) ) val inputs = Seq(if esModule then Input.ESModule(entrypoint.toPath) else Input.Script(entrypoint.toPath)) val config = RunConfig().withLogger(logger.scalaJsLogger) val processJs = envJs.start(inputs, config) processJs.future.value.foreach { case Failure(t) => throw new Exception(t) case Success(_) => } val processField = processJs.getClass.getDeclaredField("org$scalajs$jsenv$ExternalJSRun$$process") processField.setAccessible(true) val process = processField.get(processJs).asInstanceOf[Process] process } } def runNative( launcher: File, args: Seq[String], logger: Logger, allowExecve: Boolean = false, extraEnv: Map[String, String] = Map.empty ): Process = { import logger.{log, debug} val command = Seq(launcher.getAbsolutePath) ++ args log( s"Running ${command.mkString(" ")}", " Running" + System.lineSeparator() + command.iterator.map(_ + System.lineSeparator()).mkString ) if (allowExecve && Execve.available()) { debug("execve available") Execve.execve( command.head, launcher.getName +: command.tail.toArray, (sys.env ++ extraEnv).toArray.sorted.map { case (k, v) => s"$k=$v" } ) sys.error("should not happen") } else { val builder = new ProcessBuilder(command*) .inheritIO() val env = builder.environment() for ((k, v) <- extraEnv) env.put(k, v) builder.start() } } private def runTests( classPath: Seq[Path], frameworks: Seq[Framework], requireTests: Boolean, args: Seq[String], parentInspector: AsmTestRunner.ParentInspector, logger: Logger ): Either[NoTestsRun, Boolean] = frameworks .flatMap { framework => val trLogger = toTestRunnerLogger(logger) val taskDefs = AsmTestRunner.taskDefs( classPath, keepJars = false, framework.fingerprints().toIndexedSeq, parentInspector, trLogger ).toArray val runner = framework.runner(args.toArray, Array(), null) val initialTasks = runner.tasks(taskDefs) val events = TestRunner.runTasks(initialTasks.toIndexedSeq, System.out) val doneMsg = runner.done() if doneMsg.nonEmpty then System.out.println(doneMsg) events } match { case events if requireTests && events.isEmpty => Left(new NoTestsRun) case events => Right { !events.exists { ev => ev.status == Status.Error || ev.status == Status.Failure || ev.status == Status.Canceled } } } def frameworkNames( classPath: Seq[Path], parentInspector: AsmTestRunner.ParentInspector, logger: Logger ): Either[NoTestFrameworkFoundError, Seq[String]] = { val trLogger = toTestRunnerLogger(logger) logger.debug("Looking for test framework services on the classpath...") val foundFrameworkServices = AsmTestRunner.findFrameworkServices(classPath, trLogger) .map(_.replace('/', '.').replace('\\', '.')) logger.debug(s"Found ${foundFrameworkServices.length} test framework services.") if foundFrameworkServices.nonEmpty then logger.debug(s" - ${foundFrameworkServices.mkString("\n - ")}") logger.debug("Looking for more test frameworks on the classpath...") val foundFrameworks = AsmTestRunner.findFrameworks( classPath, TestRunner.commonTestFrameworks, parentInspector, trLogger ) .map(_.replace('/', '.').replace('\\', '.')) logger.debug(s"Found ${foundFrameworks.length} additional test frameworks") if foundFrameworks.nonEmpty then logger.debug(s" - ${foundFrameworks.mkString("\n - ")}") val frameworks: Seq[String] = foundFrameworkServices ++ foundFrameworks logger.log(s"Found ${frameworks.length} test frameworks in total") if frameworks.nonEmpty then logger.debug(s" - ${frameworks.mkString("\n - ")}") if frameworks.nonEmpty then Right(frameworks) else Left(new NoTestFrameworkFoundError) } def testJs( classPath: Seq[Path], entrypoint: File, requireTests: Boolean, args: Seq[String], predefinedTestFrameworks: Seq[String], logger: Logger, jsDom: Boolean, esModule: Boolean ): Either[TestError, Int] = either { import org.scalajs.jsenv.Input import org.scalajs.jsenv.nodejs.NodeJSEnv logger.debug("Preparing to run tests with Scala.js...") logger.debug(s"Scala.js tests class path: $classPath") val nodePath = findInPath("node").fold("node")(_.toString) logger.debug(s"Node found at $nodePath") val jsEnv: JSEnv = if jsDom then { logger.log("Loading JS environment with JS DOM...") new JSDOMNodeJSEnv( JSDOMNodeJSEnv.Config() .withExecutable(nodePath) .withArgs(Nil) .withEnv(Map.empty) ) } else { logger.log("Loading JS environment with Node...") new NodeJSEnv( NodeJSEnv.Config() .withExecutable(nodePath) .withArgs(Nil) .withEnv(Map.empty) .withSourceMap(NodeJSEnv.SourceMap.Disable) ) } val adapterConfig = ScalaJsTestAdapter.Config().withLogger(logger.scalaJsLogger) val inputs = Seq(if esModule then Input.ESModule(entrypoint.toPath) else Input.Script(entrypoint.toPath)) var adapter: ScalaJsTestAdapter = null logger.debug(s"JS tests class path: $classPath") val parentInspector = new AsmTestRunner.ParentInspector(classPath, toTestRunnerLogger(logger)) val foundFrameworkNames: List[String] = predefinedTestFrameworks match { case f if f.nonEmpty => f.toList case Nil => value(frameworkNames(classPath, parentInspector, logger)).toList } val res = try { adapter = new ScalaJsTestAdapter(jsEnv, inputs, adapterConfig) val loadedFrameworks = adapter .loadFrameworks(foundFrameworkNames.map(List(_))) .flatten .distinctBy(_.name()) val finalTestFrameworks = loadedFrameworks .filter( !_.name().toLowerCase.contains("junit") || !loadedFrameworks.exists(_.name().toLowerCase.contains("munit")) ) if finalTestFrameworks.nonEmpty then logger.log( s"""Final list of test frameworks found: | - ${finalTestFrameworks.map(_.description).mkString("\n - ")} |""".stripMargin ) if finalTestFrameworks.isEmpty then Left(new NoFrameworkFoundByBridgeError) else runTests(classPath, finalTestFrameworks, requireTests, args, parentInspector, logger) } finally if adapter != null then adapter.close() if value(res) then 0 else 1 } def testNative( classPath: Seq[Path], launcher: File, predefinedTestFrameworks: Seq[String], requireTests: Boolean, args: Seq[String], logger: Logger ): Either[TestError, Int] = either { logger.debug("Preparing to run tests with Scala Native...") logger.debug(s"Native tests class path: $classPath") val parentInspector = new AsmTestRunner.ParentInspector(classPath, toTestRunnerLogger(logger)) val foundFrameworkNames: List[String] = predefinedTestFrameworks match { case f if f.nonEmpty => f.toList case Nil => value(frameworkNames(classPath, parentInspector, logger)).toList } val config = ScalaNativeTestAdapter.Config() .withBinaryFile(launcher) .withEnvVars(sys.env) .withLogger(logger.scalaNativeTestLogger) var adapter: ScalaNativeTestAdapter = null val res = try { adapter = new ScalaNativeTestAdapter(config) val loadedFrameworks = adapter .loadFrameworks(foundFrameworkNames.map(List(_))) .flatten .distinctBy(_.name()) val finalTestFrameworks = loadedFrameworks // .filter( // _.name() != "Scala Native JUnit test framework" || // !loadedFrameworks.exists(_.name() == "munit") // ) // TODO: add support for JUnit and then only hardcode filtering it out when passed with munit // https://github.com/VirtusLab/scala-cli/issues/3627 .filter(_.name() != "Scala Native JUnit test framework") if finalTestFrameworks.nonEmpty then logger.log( s"""Final list of test frameworks found: | - ${finalTestFrameworks.map(_.description).mkString("\n - ")} |""".stripMargin ) val skippedFrameworks = loadedFrameworks.diff(finalTestFrameworks) if skippedFrameworks.nonEmpty then logger.log( s"""The following test frameworks have been filtered out: | - ${skippedFrameworks.map(_.description).mkString("\n - ")} |""".stripMargin ) if finalTestFrameworks.isEmpty then Left(new NoFrameworkFoundByNativeBridgeError) else runTests(classPath, finalTestFrameworks, requireTests, args, parentInspector, logger) } finally if adapter != null then adapter.close() if value(res) then 0 else 1 } } ================================================ FILE: modules/build/src/main/scala/scala/build/internal/WrapperUtils.scala ================================================ package scala.build.internal import scala.build.internal.util.WarningMessages object WrapperUtils { enum ScriptMainMethod: case Exists(name: String) case Multiple(names: Seq[String]) case ToplevelStatsPresent case ToplevelStatsWithMultiple(names: Seq[String]) case NoMain def warningMessage: List[String] = this match case ScriptMainMethod.Multiple(names) => List(WarningMessages.multipleMainObjectsInScript(names)) case ScriptMainMethod.ToplevelStatsPresent => List( WarningMessages.mixedToplvelAndObjectInScript ) case ToplevelStatsWithMultiple(names) => List( WarningMessages.multipleMainObjectsInScript(names), WarningMessages.mixedToplvelAndObjectInScript ) case _ => Nil def mainObjectInScript(scalaVersion: String, code: String): ScriptMainMethod = import scala.meta.* val scriptDialect = if scalaVersion.startsWith("3") then dialects.Scala3Future else dialects.Scala213Source3 given Dialect = scriptDialect.withAllowToplevelStatements(true).withAllowToplevelTerms(true) val parsedCode = code.parse[Source] match case Parsed.Success(Source(stats)) => stats case _ => Nil // Check if there is a main function defined inside an object def checkSignature(defn: Defn.Def) = defn.paramClauseGroups match case List(Member.ParamClauseGroup( Type.ParamClause(Nil), List(Term.ParamClause( List(Term.Param( Nil, _: Term.Name, Some(Type.Apply.After_4_6_0( Type.Name("Array"), Type.ArgClause(List(Type.Name("String"))) )), None )), None )) )) => true case _ => false def noToplevelStatements = parsedCode.forall { case _: Term => false case _ => true } def hasMainSignature(templ: Template) = templ.body.stats.exists { case defn: Defn.Def => defn.name.value == "main" && checkSignature(defn) case _ => false } def extendsApp(templ: Template) = templ.inits match case Init.After_4_6_0(Type.Name("App"), _, Nil) :: Nil => true case _ => false val potentialMains = parsedCode.collect { case Defn.Object(_, objName, templ) if extendsApp(templ) || hasMainSignature(templ) => Seq(objName.value) }.flatten potentialMains match case head :: Nil if noToplevelStatements => ScriptMainMethod.Exists(head) case head :: Nil => ScriptMainMethod.ToplevelStatsPresent case Nil => ScriptMainMethod.NoMain case seq if noToplevelStatements => ScriptMainMethod.Multiple(seq) case seq => ScriptMainMethod.ToplevelStatsWithMultiple(seq) } ================================================ FILE: modules/build/src/main/scala/scala/build/internal/markdown/MarkdownCodeBlock.scala ================================================ package scala.build.internal.markdown import scala.annotation.tailrec import scala.build.errors.BuildException import scala.jdk.CollectionConverters.* /** Representation for a (closed) code block contained in Markdown * * @param info * a list of tags tied to a given code block * @param body * the code block content * @param startLine * starting line on which the code block was defined (excluding backticks) * @param endLine * end line on which the code block was closed (excluding backticks) */ case class MarkdownCodeBlock( info: Seq[String], body: String, startLine: Int, endLine: Int ) { /** @return * `true` if this snippet should be ignored, `false` otherwise */ def shouldIgnore: Boolean = info.head != "scala" || info.contains("ignore") /** @return * `true` if this snippet should have its scope reset, `false` otherwise */ def resetScope: Boolean = info.contains("reset") /** @return * `true` if this snippet is a test snippet, `false` otherwise */ def isTest: Boolean = info.contains("test") /** @return * `true` if this snippet is a raw snippet, `false` otherwise */ def isRaw: Boolean = info.contains("raw") } object MarkdownCodeBlock { /** Finds all code snippets in given input * * @param subPath * the project [[os.SubPath]] to the Markdown file * @param md * Markdown file in a `String` format * @param maybeRecoverOnError * function potentially recovering on errors * @return * list of all found snippets */ def findCodeBlocks( subPath: os.SubPath, md: String, maybeRecoverOnError: BuildException => Option[BuildException] = Some(_) ): Either[BuildException, Seq[MarkdownCodeBlock]] = { val allLines = md .lines() .toList .asScala @tailrec def findCodeBlocksRec( lines: Seq[String], closedFences: Seq[MarkdownCodeBlock] = Seq.empty, maybeOpenFence: Option[MarkdownOpenFence] = None, currentIndex: Int = 0 ): Either[BuildException, Seq[MarkdownCodeBlock]] = lines -> maybeOpenFence match { case (Seq(currentLine, tail*), mof) => val (newClosedFences, newOpenFence) = mof match { case None => closedFences -> MarkdownOpenFence.maybeFence(currentLine, currentIndex) case Some(openFence) => val backticksStart = currentLine.indexOf(openFence.backticks) if backticksStart == openFence.indent && currentLine.forall(c => c == '`' || c.isWhitespace) then (closedFences :+ openFence.closeFence(currentIndex, allLines.toArray)) -> None else closedFences -> Some(openFence) } findCodeBlocksRec(tail, newClosedFences, newOpenFence, currentIndex + 1) case (Nil, Some(openFence)) => maybeRecoverOnError(openFence.toUnclosedBackticksError(os.pwd / subPath)) .map(e => Left(e)) .getOrElse(Right(closedFences)) case _ => Right(closedFences) } findCodeBlocksRec(allLines.toSeq).map(_.filter(!_.shouldIgnore)) } } ================================================ FILE: modules/build/src/main/scala/scala/build/internal/markdown/MarkdownCodeWrapper.scala ================================================ package scala.build.internal.markdown import scala.annotation.tailrec import scala.build.internal.{AmmUtil, Name} import scala.build.preprocessing.{ ExtractedDirectives, PreprocessedMarkdown, PreprocessedMarkdownCodeBlocks } /** A util for extraction and wrapping of code blocks in Markdown files. */ object MarkdownCodeWrapper { case class WrappedMarkdownCode( code: String, directives: ExtractedDirectives = ExtractedDirectives.empty ) /** Extracts scala code blocks from Markdown snippets, divides them into 3 categories and wraps * when necessary. * * @param subPath * the project [[os.SubPath]] to the Markdown file * @param markdown * preprocessed Markdown code blocks * @return * a tuple of (Option(simple scala code blocks), Option(raw scala snippets code blocks), * Option(test scala snippets code blocks)) */ def apply( subPath: os.SubPath, markdown: PreprocessedMarkdown ): (Option[WrappedMarkdownCode], Option[WrappedMarkdownCode], Option[WrappedMarkdownCode]) = { val (pkg, wrapper) = AmmUtil.pathToPackageWrapper(subPath) val maybePkgString = if pkg.isEmpty then None else Some(s"package ${AmmUtil.encodeScalaSourcePath(pkg)}") val wrapperName = Name(s"${wrapper.raw}_md").backticked ( wrapScalaCode(markdown.scriptCodeBlocks, wrapperName, maybePkgString), rawScalaCode(markdown.rawCodeBlocks), rawScalaCode(markdown.testCodeBlocks) ) } /** Scope object name for a given index * @param index * scope index * @return * scope name */ private def scopeObjectName(index: Int): String = if index != 0 then s"Scope$index" else "Scope" /** Transforms [[MarkdownCodeBlock]] code blocks into code in the [[Option]] of String format * * @param snippets * extracted [[MarkdownCodeBlock]] code blocks * @param f * a function transforming a sequence of code blocks into a single String of code * @return * an Option of the resulting code String, if any */ private def code( snippets: Seq[MarkdownCodeBlock], f: Seq[MarkdownCodeBlock] => String ): Option[String] = if snippets.isEmpty then None else Some(AmmUtil.normalizeNewlines(f(snippets))) /** Wraps plain `scala` snippets in relevant scope objects, forming a script-like wrapper. * * @param snippets * a sequence of code blocks * @param wrapperName * name for the wrapper object * @param pkg * package for the wrapper object * @return * an option of the wrapped code String */ def wrapScalaCode( preprocessed: PreprocessedMarkdownCodeBlocks, wrapperName: String, pkg: Option[String] ): Option[WrappedMarkdownCode] = code( preprocessed.codeBlocks, s => { val packageDirective = pkg.map(_ + "; ").getOrElse("") val noWarnAnnotation = """@annotation.nowarn("msg=pure expression does nothing")""" val firstLine = s"""${packageDirective}object $wrapperName { $noWarnAnnotation def main(args: Array[String]): Unit = { """ s.indices.foldLeft(0 -> firstLine) { case ((nextScopeIndex, sum), index) => if preprocessed.codeBlocks(index).resetScope || index == 0 then nextScopeIndex + 1 -> (sum :++ s"${scopeObjectName(nextScopeIndex)}; ") else nextScopeIndex -> sum // that class hasn't been created } ._2 .:++("}") .:++(generateMainScalaLines(s, 0, 0, 0)) .:++("}") } ).map(c => WrappedMarkdownCode(c, preprocessed.extractedDirectives)) @tailrec private def generateMainScalaLines( snippets: Seq[MarkdownCodeBlock], index: Int, scopeIndex: Int, line: Int, acc: String = "" ): String = if (index >= snippets.length) s"$acc}" // close last class else { val fence: MarkdownCodeBlock = snippets(index) val classOpener: String = if (index == 0) s"object ${scopeObjectName(scopeIndex)} {${AmmUtil.lineSeparator}" // first snippet needs to open a class else if (fence.resetScope) s"}; object ${scopeObjectName(scopeIndex)} {${AmmUtil.lineSeparator}" // if scope is being reset, close previous class and open a new one else AmmUtil.lineSeparator val nextScopeIndex = if index == 0 || fence.resetScope then scopeIndex + 1 else scopeIndex val newAcc = acc + (AmmUtil.lineSeparator * (fence.startLine - line - 1)) // padding .:++(classOpener) // new class opening (if applicable) .:++(fence.body) // snippet body .:++(AmmUtil.lineSeparator) // padding in place of closing backticks generateMainScalaLines( snippets = snippets, index = index + 1, scopeIndex = nextScopeIndex, line = fence.endLine + 1, acc = newAcc ) } /** Glues raw Scala snippets into a single file. * * @param snippets * a sequence of code blocks * @return * an option of the resulting code String */ def rawScalaCode(preprocessed: PreprocessedMarkdownCodeBlocks): Option[WrappedMarkdownCode] = code(preprocessed.codeBlocks, generateRawScalaLines(_, 0, 0)) .map(c => WrappedMarkdownCode(c, preprocessed.extractedDirectives)) @tailrec private def generateRawScalaLines( snippets: Seq[MarkdownCodeBlock], index: Int, line: Int, acc: String = "" ): String = if index >= snippets.length then acc else { val fence: MarkdownCodeBlock = snippets(index) val newAcc = acc + (AmmUtil.lineSeparator * (fence.startLine - line)) // padding .:++(fence.body) // snippet body .:++(AmmUtil.lineSeparator) // padding in place of closing backticks generateRawScalaLines(snippets, index + 1, fence.endLine + 1, newAcc) } } ================================================ FILE: modules/build/src/main/scala/scala/build/internal/markdown/MarkdownOpenFence.scala ================================================ package scala.build.internal.markdown import scala.build.Position import scala.build.errors.MarkdownUnclosedBackticksError import scala.build.preprocessing.SheBang /** Representation for an open code block in Markdown. (open meaning the closing backticks haven't * yet been parsed or they aren't at all present) * * @param info * a list of tags tied to a given code block * @param tickStartLine * index of the starting line on which the opening backticks were defined * @param backticks * the backticks string opening the fence * @param indent * number of spaces of indentation for the fence */ case class MarkdownOpenFence( info: String, tickStartLine: Int, // fence start INCLUDING backticks backticks: String, indent: Int ) { /** Closes started code-fence * * @param tickEndLine * number of the line where closing backticks are * @param lines * input file sliced into lines * @return * [[MarkdownCodeBlock]] representing whole closed code-fence */ def closeFence( tickEndLine: Int, lines: Array[String] ): MarkdownCodeBlock = { val start: Int = tickStartLine + 1 val bodyLines: Array[String] = lines.slice(start, tickEndLine) val body = bodyLines.mkString("\n") val (bodyWithNoSheBang, _, _) = SheBang.ignoreSheBangLines(body) MarkdownCodeBlock( info.split("\\s+").toList, // strip info by whitespaces bodyWithNoSheBang, start, // snippet has to begin in the new line tickEndLine - 1 // ending backticks have to be placed below the snippet ) } /** Converts the [[MarkdownOpenFence]] into a [[MarkdownUnclosedBackticksError]] * * @param mdPath * path to the Markdown file * @return * a [[MarkdownUnclosedBackticksError]] */ def toUnclosedBackticksError(mdPath: os.Path): MarkdownUnclosedBackticksError = { val startCoordinates = tickStartLine -> indent val endCoordinates = tickStartLine -> (indent + backticks.length) val position = Position.File(Right(mdPath), startCoordinates, endCoordinates) MarkdownUnclosedBackticksError(backticks, Seq(position)) } } object MarkdownOpenFence { def maybeFence(line: String, index: Int): Option[MarkdownOpenFence] = { val start: Int = line.indexOf("```") if (start >= 0) { val fence = line.substring(start) val backticks: String = fence.takeWhile(_ == '`') val info: String = fence.substring(backticks.length) Some(MarkdownOpenFence(info, index, backticks, start)) } else None } } ================================================ FILE: modules/build/src/main/scala/scala/build/internal/resource/NativeResourceMapper.scala ================================================ package scala.build.internal.resource import scala.build.Build import scala.build.input.CFile object NativeResourceMapper { private def scalaNativeCFileMapping(build: Build.Successful): Map[os.Path, os.RelPath] = build .inputs .flattened() .collect { case cfile: CFile => val inputPath = cfile.path val destPath = os.rel / "scala-native" / cfile.subPath (inputPath, destPath) } .toMap private def resolveProjectCFileRegistryPath(nativeWorkDir: os.Path) = nativeWorkDir / ".native_registry" /** Copies and maps c file resources from their original path to the destination path in build * output, also caching output paths in a file. * * Remembering the mapping this way allows for the resource to be removed if the original file is * renamed/deleted. */ def copyCFilesToScalaNativeDir(build: Build.Successful, nativeWorkDir: os.Path): Unit = { val mappingFilePath = resolveProjectCFileRegistryPath(nativeWorkDir) ResourceMapper.copyResourcesToDirWithMapping( build.output, mappingFilePath, scalaNativeCFileMapping(build) ) } } ================================================ FILE: modules/build/src/main/scala/scala/build/internal/resource/ResourceMapper.scala ================================================ package scala.build.internal.resource import scala.build.Build import scala.build.internal.Constants object ResourceMapper { private def resourceMapping(build: Build.Successful): Map[os.Path, os.RelPath] = { val seq = for { resourceDirPath <- build.sources.resourceDirs.filter(os.exists(_)) resourceFilePath <- os.walk(resourceDirPath).filter(os.isFile(_)) relativeResourcePath = resourceFilePath.relativeTo(resourceDirPath) // dismiss files generated by scala-cli if !relativeResourcePath.startsWith(os.rel / Constants.workspaceDirName) } yield (resourceFilePath, relativeResourcePath) seq.toMap } def copyResourcesToDirWithMapping( output: os.Path, registryFilePath: os.Path, newMapping: Map[os.Path, os.RelPath] ): Unit = { val oldRegistry = if (os.exists(registryFilePath)) os.read(registryFilePath) .linesIterator .filter(_.nonEmpty) .map(os.RelPath(_)) .toSet else Set.empty val removedFiles = oldRegistry -- newMapping.values for (f <- removedFiles) os.remove(output / f) for ((inputPath, outputPath) <- newMapping) os.copy( inputPath, output / outputPath, replaceExisting = true, createFolders = true ) if (newMapping.isEmpty) os.remove(registryFilePath) else os.write.over( registryFilePath, newMapping.map(_._2.toString).toVector.sorted.mkString("\n") ) } /** Copies and maps resources from their original path to the destination path in build output, * also caching output paths in a file. * * Remembering the mapping this way allows for the resource to be removed if the original file is * renamed/deleted. */ def copyResourceToClassesDir(build: Build): Unit = build match { case b: Build.Successful => val fullFilePath = Build.resourcesRegistry(b.inputs.workspace, b.inputs.projectName, b.scope) copyResourcesToDirWithMapping(b.output, fullFilePath, resourceMapping(b)) case _ => } } ================================================ FILE: modules/build/src/main/scala/scala/build/internal/util/RegexUtils.scala ================================================ package scala.build.internal.util import java.util.regex.Pattern object RegexUtils { /** Based on junit-interface [GlobFilter. * compileGlobPattern](https://github.com/sbt/junit-interface/blob/f8c6372ed01ce86f15393b890323d96afbe6d594/src/main/java/com/novocode/junit/GlobFilter.java#L37) * * @return * Pattern allows to regex input which contains only *, for example `*foo*` match to * `MyTests.foo` */ def globPattern(expr: String): Pattern = { val a = expr.split("\\*", -1) val b = new StringBuilder() for (i <- 0 until a.length) { if (i != 0) b.append(".*") if (a(i).nonEmpty) b.append(Pattern.quote(a(i).replaceAll("\n", "\\n"))) } Pattern.compile(b.toString) } } ================================================ FILE: modules/build/src/main/scala/scala/build/internal/util/WarningMessages.scala ================================================ package scala.build.internal.util import scala.build.input.ScalaCliInvokeData import scala.build.internal.Constants import scala.build.internals.FeatureType import scala.build.preprocessing.directives.{DirectiveHandler, ScopedDirective} import scala.cli.commands.SpecificationLevel import scala.cli.config.Key object WarningMessages { private val scalaCliGithubUrl = s"https://github.com/${Constants.ghOrg}/${Constants.ghName}" private val experimentalNote = s"""Please bear in mind that non-ideal user experience should be expected. |If you encounter any bugs or have feedback to share, make sure to reach out to the maintenance team at $scalaCliGithubUrl""".stripMargin def experimentalFeaturesUsed(namesAndFeatureTypes: Seq[(String, FeatureType)]): String = { val message = namesAndFeatureTypes match { case Seq((name, featureType)) => s"The `$name` $featureType is experimental" case namesAndTypes => val nl = System.lineSeparator() val distinctFeatureTypes = namesAndTypes.map(_._2).distinct val (bulletPointList, featureNameToPrint) = if (distinctFeatureTypes.size == 1) ( namesAndTypes.map((name, _) => s" - `$name`") .mkString(nl), s"${distinctFeatureTypes.head}s" // plural form ) else ( namesAndTypes.map((name, fType) => s" - `$name` $fType") .mkString(nl), "features" ) s"""Some utilized $featureNameToPrint are marked as experimental: |$bulletPointList""".stripMargin } s"""$message |$experimentalNote""".stripMargin } def experimentalSubcommandWarning(name: String): String = s"""The `$name` sub-command is experimental. |$experimentalNote""".stripMargin def rawValueNotWrittenToPublishFile( rawValue: String, valueName: String, directiveName: String ): String = s"""The value of $valueName ${Console.BOLD}will not${Console.RESET} be written to a potentially public file! |Provide it as an option to the publish subcommand with: | $directiveName value:$rawValue |""".stripMargin /** Using @main is impossible in new [[scala.build.internal.ClassCodeWrapper]] since none of the * definitions can be accessed statically, so those errors are swapped with this text * @param annotationIgnored * will annotation be ignored (or will compilation fail) */ def mainAnnotationNotSupported(annotationIgnored: Boolean): String = val consequencesString = if annotationIgnored then s", it will be ignored" else "" s"Annotation @main in .sc scripts is not supported$consequencesString, use .scala format instead" private def powerFeatureUsedInSip( featureName: String, featureType: String, specificationLevel: SpecificationLevel )(using invokeData: ScalaCliInvokeData): String = { val powerType = if specificationLevel == SpecificationLevel.EXPERIMENTAL then "experimental" else "restricted" s"""The `$featureName` $featureType is $powerType. |You can run it with the `--power` flag or turn power mode on globally by running: | ${Console.BOLD}${invokeData.progName} config power true${Console.RESET}""".stripMargin } def powerCommandUsedInSip(commandName: String, specificationLevel: SpecificationLevel)(using ScalaCliInvokeData ): String = powerFeatureUsedInSip(commandName, "sub-command", specificationLevel) def powerOptionUsedInSip(optionName: String, specificationLevel: SpecificationLevel)(using ScalaCliInvokeData ): String = powerFeatureUsedInSip(optionName, "option", specificationLevel) def powerConfigKeyUsedInSip(key: Key[?])(using ScalaCliInvokeData): String = powerFeatureUsedInSip(key.fullName, "configuration key", key.specificationLevel) def powerDirectiveUsedInSip( directive: ScopedDirective, handler: DirectiveHandler[?] )(using ScalaCliInvokeData): String = powerFeatureUsedInSip( directive.directive.toString, "directive", handler.scalaSpecificationLevel ) val chainingUsingFileDirective: String = "Chaining the 'using file' directive is not supported, the source won't be included in the build." val offlineModeBloopNotFound = "Offline mode is ON and Bloop could not be fetched from the local cache, using scalac as fallback" val offlineModeBloopJvmNotFound = "Offline mode is ON and a JVM for Bloop could not be fetched from the local cache, using scalac as fallback" def multipleMainObjectsInScript(names: Seq[String]) = s"Only a single main is allowed within scripts. Multiple main classes were found in the script: ${names.mkString(", ")}" def mixedToplvelAndObjectInScript = "Script contains objects with main methods and top-level statements, only the latter will be run." def directivesInMultipleFilesWarning( projectFilePath: String, pathsToReport: Iterable[String] = Nil ) = { val detectedMsg = "Using directives detected in multiple files" val recommendedMsg = s"It is recommended to keep them centralized in the $projectFilePath file." if pathsToReport.isEmpty then s"$detectedMsg. $recommendedMsg" else s"""$detectedMsg: |${pathsToReport.mkString("- ", s"${System.lineSeparator}- ", "")} |$recommendedMsg |""".stripMargin } val mainScriptNameClashesWithAppWrapper = "Script file named 'main.sc' detected, keep in mind that accessing it from other scripts is impossible due to a clash of `main` symbols" def deprecatedWarning(old: String, `new`: String) = s"Using '$old' is deprecated, use '${`new`}' instead" def deprecatedToolkitLatest(updatedValue: String = "") = if updatedValue.isEmpty then """Using 'latest' for toolkit is deprecated, use 'default' to get more stable behaviour""" else s"""Using 'latest' for toolkit is deprecated, use 'default' to get more stable behaviour: | $updatedValue""".stripMargin } ================================================ FILE: modules/build/src/main/scala/scala/build/internal/zip/WrappedZipInputStream.scala ================================================ package scala.build.internal.zip import java.io.{Closeable, InputStream} import java.util.zip.ZipEntry import scala.build.internals.EnvVar /* * juz.ZipInputStream is buggy on Arch Linux from native images (CRC32 calculation issues, * see oracle/graalvm#4479), so we use a custom ZipInputStream with disabled CRC32 calculation. */ final case class WrappedZipInputStream( wrapped: Either[io.github.scala_cli.zip.ZipInputStream, java.util.zip.ZipInputStream] ) extends Closeable { def entries(): Iterator[ZipEntry] = { val getNextEntry = wrapped match { case Left(zis) => () => zis.getNextEntry() case Right(zis) => () => zis.getNextEntry() } Iterator.continually(getNextEntry()).takeWhile(_ != null) } def closeEntry(): Unit = wrapped match { case Left(zis) => zis.closeEntry() case Right(zis) => zis.closeEntry() } def readAllBytes(): Array[Byte] = wrapped.merge.readAllBytes() def close(): Unit = wrapped.merge.close() } object WrappedZipInputStream { lazy val shouldUseVendoredImplem = { def toBoolean(input: String): Boolean = input match { case "true" | "1" => true case _ => false } EnvVar.ScalaCli.vendoredZipInputStream.valueOpt.map(toBoolean) .orElse(sys.props.get("scala-cli.zis.vendored").map(toBoolean)) .getOrElse(false) } def create(is: InputStream): WrappedZipInputStream = WrappedZipInputStream { if (shouldUseVendoredImplem) Left(new io.github.scala_cli.zip.ZipInputStream(is)) else Right(new java.util.zip.ZipInputStream(is)) } } ================================================ FILE: modules/build/src/main/scala/scala/build/package.scala ================================================ package scala.build import scala.annotation.tailrec import scala.concurrent.duration.{DurationInt, FiniteDuration} import scala.util.Random def retry[T]( maxAttempts: Int = 3, waitDuration: FiniteDuration = 1.seconds, variableWaitDelayInMs: Int = 500 )(logger: Logger)( run: => T ): T = { @tailrec def helper(count: Int): T = try run catch { case t: Throwable => if count >= maxAttempts then throw t else logger.debugStackTrace(t) val variableDelay = Random.between(0, variableWaitDelayInMs + 1).milliseconds val currentWaitDuration = waitDuration + variableDelay logger.log(s"Caught $t, trying again in $currentWaitDuration…") Thread.sleep(currentWaitDuration.toMillis) helper(count + 1) } helper(1) } ================================================ FILE: modules/build/src/main/scala/scala/build/postprocessing/AsmPositionUpdater.scala ================================================ package scala.build.postprocessing import org.objectweb.asm import java.nio.file.{FileAlreadyExistsException, NoSuchFileException} import scala.build.{Logger, Os, retry} object AsmPositionUpdater { private class LineNumberTableMethodVisitor( lineShift: Int, delegate: asm.MethodVisitor ) extends asm.MethodVisitor(asm.Opcodes.ASM9, delegate) { override def visitLineNumber(line: Int, start: asm.Label): Unit = super.visitLineNumber(line + lineShift, start) } private class LineNumberTableClassVisitor( mappings: Map[String, (String, Int)], cw: asm.ClassWriter ) extends asm.ClassVisitor(asm.Opcodes.ASM9, cw) { private var lineShiftOpt = Option.empty[Int] def mappedStuff = lineShiftOpt.nonEmpty override def visitSource(source: String, debug: String): Unit = mappings.get(source) match { case None => super.visitSource(source, debug) case Some((newSource, lineShift)) => lineShiftOpt = Some(lineShift) super.visitSource(newSource, debug) } override def visitMethod( access: Int, name: String, descriptor: String, signature: String, exceptions: Array[String] ): asm.MethodVisitor = { val main = super.visitMethod(access, name, descriptor, signature, exceptions) lineShiftOpt match { case None => main case Some(lineShift) => new LineNumberTableMethodVisitor(lineShift, main) } } } def postProcess( mappings: Map[String, (String, Int)], output: os.Path, logger: Logger ): Unit = { os.walk(output) .iterator .filter(os.isFile(_)) .filter(_.last.endsWith(".class")) .foreach { path => try retry()(logger) { val is = os.read.inputStream(path) val updateByteCodeOpt = try retry()(logger) { val reader = new asm.ClassReader(is) val writer = new asm.ClassWriter(reader, 0) val checker = new LineNumberTableClassVisitor(mappings, writer) reader.accept(checker, 0) if checker.mappedStuff then Some(writer.toByteArray) else None } catch { case e: ArrayIndexOutOfBoundsException => e.getStackTrace.foreach(ste => logger.debug(ste.toString)) logger.log(s"Error while processing ${path.relativeTo(Os.pwd)}: $e.") logger.log("Are you trying to run too many builds at once? Trying to recover...") None } finally is.close() for (b <- updateByteCodeOpt) { logger.debug(s"Overwriting ${path.relativeTo(Os.pwd)}") os.write.over(path, b) } } catch { case e: (NoSuchFileException | FileAlreadyExistsException | ArrayIndexOutOfBoundsException) => logger.debugStackTrace(e) logger.log(s"Error while processing ${path.relativeTo(Os.pwd)}: $e") logger.log("Are you trying to run too many builds at once? Trying to recover...") } } } } ================================================ FILE: modules/build/src/main/scala/scala/build/postprocessing/ByteCodePostProcessor.scala ================================================ package scala.build.postprocessing import scala.build.options.BuildOptions import scala.build.{GeneratedSource, Logger} import scala.util.{Either, Right} case object ByteCodePostProcessor extends PostProcessor { def postProcess( generatedSources: Seq[GeneratedSource], mappings: Map[String, (String, Int)], workspace: os.Path, output: os.Path, logger: Logger, scalaVersion: String, buildOptions: BuildOptions ): Either[String, Unit] = Right(AsmPositionUpdater.postProcess(mappings, output, logger)) } ================================================ FILE: modules/build/src/main/scala/scala/build/postprocessing/LineConversion.scala ================================================ package scala.build.postprocessing import scala.build.internal.WrapperParams object LineConversion { def scalaLineToScLine(lineScala: Int, wrapperParamsOpt: Option[WrapperParams]): Option[Int] = wrapperParamsOpt match { case Some(wrapperParams) => val lineSc = lineScala - wrapperParams.topWrapperLineCount if (lineSc >= 0 && lineSc < wrapperParams.userCodeLineCount) Some(lineSc) else None case _ => None } def scalaLineToScLineShift(wrapperParamsOpt: Option[WrapperParams]): Int = wrapperParamsOpt match { case Some(wrapperParams) => wrapperParams.topWrapperLineCount * -1 case _ => 0 } } ================================================ FILE: modules/build/src/main/scala/scala/build/postprocessing/PostProcessor.scala ================================================ package scala.build.postprocessing import scala.build.options.BuildOptions import scala.build.{GeneratedSource, Logger} trait PostProcessor { def postProcess( generatedSources: Seq[GeneratedSource], mappings: Map[String, (String, Int)], workspace: os.Path, output: os.Path, logger: Logger, scalaVersion: String, buildOptions: BuildOptions ): Either[String, Unit] } ================================================ FILE: modules/build/src/main/scala/scala/build/postprocessing/SemanticDbPostProcessor.scala ================================================ package scala.build.postprocessing import java.nio.file.FileSystemException import scala.annotation.tailrec import scala.build.options.BuildOptions import scala.build.postprocessing.LineConversion.scalaLineToScLine import scala.build.{GeneratedSource, Logger} import scala.util.{Either, Right, Try} case object SemanticDbPostProcessor extends PostProcessor { def postProcess( generatedSources: Seq[GeneratedSource], mappings: Map[String, (String, Int)], workspace: os.Path, output: os.Path, logger: Logger, scalaVersion: String, buildOptions: BuildOptions ): Either[String, Unit] = Right { logger.debug("Moving semantic DBs around") val semanticDbOptions = buildOptions.scalaOptions.semanticDbOptions val semDbSourceRoot = semanticDbOptions.semanticDbSourceRoot.getOrElse(workspace) val semDbTargetRoot = semanticDbOptions.semanticDbTargetRoot.getOrElse(output) / "META-INF" / "semanticdb" for (source <- generatedSources; originalSource <- source.reportingPath) { val actual = originalSource.relativeTo(semDbSourceRoot) val generatedSourceParent = os.Path(source.generated.toNIO.getParent) val potentialSemDbFile = generatedSourceParent / (source.generated.last + ".semanticdb") Some(potentialSemDbFile) .filter(os.exists) .orElse(Some(semDbTargetRoot / potentialSemDbFile.relativeTo(semDbSourceRoot))) .filter(os.exists) .foreach { semDbFile => val finalSemDbFile = { val dirSegments = actual.segments.dropRight(1) semDbTargetRoot / dirSegments / s"${actual.last}.semanticdb" } SemanticdbProcessor.postProcess( os.read(originalSource), originalSource.relativeTo(semDbSourceRoot), scalaLine => scalaLineToScLine(scalaLine, source.wrapperParamsOpt), semDbFile, finalSemDbFile ) try os.remove(semDbFile) catch { case ex: FileSystemException => logger.debug(s"Ignoring $ex while removing $semDbFile") } Try(semDbTargetRoot -> semDbFile.relativeTo(semDbTargetRoot).asSubPath).toOption .foreach { (base, subPath) => deleteSubPathIfEmpty(base, subPath / os.up, logger) } } } } @tailrec private def deleteSubPathIfEmpty(base: os.Path, subPath: os.SubPath, logger: Logger): Unit = if (subPath.segments.nonEmpty) { val p = base / subPath if (os.isDir(p) && os.list.stream(p).headOption.isEmpty) { try os.remove(p) catch { case e: FileSystemException => logger.debug(s"Ignoring $e while cleaning up $p") } deleteSubPathIfEmpty(base, subPath / os.up, logger) } } } ================================================ FILE: modules/build/src/main/scala/scala/build/postprocessing/SemanticdbProcessor.scala ================================================ // adapted from https://github.com/com-lihaoyi/Ammonite/blob/2da846d2313f1e12e812802babf9c69005f5d44a/amm/interp/src/main/scala/ammonite/interp/script/SemanticdbProcessor.scala package scala.build.postprocessing import java.math.BigInteger import java.nio.charset.StandardCharsets import java.security.MessageDigest import scala.collection.mutable import scala.meta.internal.semanticdb.* object SemanticdbProcessor { def postProcess( originalCode: String, originalPath: os.RelPath, adjust: Int => Option[Int], orig: os.Path, dest: os.Path ): Unit = { val mapRange = { (range: scala.meta.internal.semanticdb.Range) => for { startLine <- adjust(range.startLine) endLine <- adjust(range.endLine) } yield range .withStartLine(startLine) .withEndLine(endLine) } def updateTrees(trees: Seq[Tree]): Option[Seq[Tree]] = trees .foldLeft(Option(new mutable.ListBuffer[Tree])) { (accOpt, t) => for (acc <- accOpt; t0 <- updateTree(t)) yield acc += t0 } .map(_.result()) def updateTree(tree: Tree): Option[Tree] = tree match { case a: ApplyTree => for { function <- updateTree(a.function) args <- updateTrees(a.arguments) } yield a.withFunction(function).withArguments(args) case Tree.Empty => Some(Tree.Empty) case f: FunctionTree => for { body <- updateTree(f.body) } yield f.withBody(body) case i: IdTree => Some(i) case l: LiteralTree => Some(l) case m: MacroExpansionTree => for { beforeExp <- updateTree(m.beforeExpansion) } yield m.withBeforeExpansion(beforeExp) case o: OriginalTree => if (o.range.isEmpty) Some(o) else for { range <- o.range.flatMap(mapRange) } yield o.withRange(range) case s: SelectTree => for { qual <- updateTree(s.qualifier) } yield s.withQualifier(qual) case t: TypeApplyTree => for { fun <- updateTree(t.function) } yield t.withFunction(fun) } if (os.isFile(orig)) { val docs = TextDocuments.parseFrom(os.read.bytes(orig)) val updatedDocs = docs.withDocuments { docs.documents.map { doc => doc .withText(originalCode) .withUri(originalPath.toString) .withMd5(md5(originalCode)) .withDiagnostics { doc.diagnostics.flatMap { diag => diag.range.fold(Option(diag)) { range => mapRange(range).map(diag.withRange) } } } .withOccurrences { doc.occurrences.flatMap { occurrence => occurrence.range.fold(Option(occurrence)) { range => mapRange(range).map(occurrence.withRange) } } } .withSynthetics { doc.synthetics.flatMap { syn => val synOpt = syn.range.fold(Option(syn)) { range => mapRange(range).map(syn.withRange) } synOpt.flatMap { syn0 => updateTree(syn0.tree) .map(syn0.withTree) } } } } } os.write.over(dest, updatedDocs.toByteArray, createFolders = true) } else System.err.println(s"Error: $orig not found (for $dest)") } private def md5(content: String): String = { val md = MessageDigest.getInstance("MD5") val digest = md.digest(content.getBytes(StandardCharsets.UTF_8)) val res = new BigInteger(1, digest).toString(16) if (res.length < 32) ("0" * (32 - res.length)) + res else res } } ================================================ FILE: modules/build/src/main/scala/scala/build/postprocessing/TastyPostProcessor.scala ================================================ package scala.build.postprocessing import java.nio.file.{FileAlreadyExistsException, NoSuchFileException} import scala.build.internal.Constants import scala.build.options.BuildOptions import scala.build.tastylib.{TastyData, TastyVersions} import scala.build.{GeneratedSource, Logger, retry} case object TastyPostProcessor extends PostProcessor { def postProcess( generatedSources: Seq[GeneratedSource], mappings: Map[String, (String, Int)], workspace: os.Path, output: os.Path, logger: Logger, scalaVersion: String, buildOptions: BuildOptions ): Either[String, Unit] = { def updatedPaths = generatedSources .flatMap { source => source.reportingPath.toOption.toSeq.map { originalSource => val fromSourceRoot = source.generated.relativeTo(workspace) val actual = originalSource.relativeTo(workspace) fromSourceRoot.toString -> actual.toString } } .toMap TastyVersions.shouldRunPreprocessor( scalaVersion, Constants.version, buildOptions.scalaOptions.defaultScalaVersion ) match { case Right(false) => Right(()) case Left(msg) => if (updatedPaths.isEmpty) Right(()) else Left(msg) case Right(true) => val paths = updatedPaths if (paths.isEmpty) Right(()) else Right( os.walk(output) .filter(os.isFile(_)) .filter(_.last.endsWith(".tasty")) // make that case-insensitive just in case? .foreach(updateTastyFile(logger, paths)) ) } } private def updateTastyFile( logger: Logger, updatedPaths: Map[String, String] )(f: os.Path): Unit = { logger.debug(s"Reading TASTy file $f") try retry()(logger) { val content = os.read.bytes(f) TastyData.read(content) match { case Left(ex) => logger.debug(s"Ignoring exception during TASty postprocessing: $ex") case Right(data) => logger.debug(s"Parsed TASTy file $f") var updatedOne = false val updatedData = data.mapNames { n => updatedPaths.get(n) match { case Some(newName) => updatedOne = true newName case None => n } } if updatedOne then { logger.debug( s"Overwriting ${if f.startsWith(os.pwd) then f.relativeTo(os.pwd) else f}" ) val updatedContent = TastyData.write(updatedData) os.write.over(f, updatedContent) } } } catch { case e: (NoSuchFileException | FileAlreadyExistsException | ArrayIndexOutOfBoundsException) => logger.debugStackTrace(e) logger.log(s"Tasty file $f not found: $e. Are you trying to run too many builds at once") logger.log("Are you trying to run too many builds at once? Trying to recover...") } } } ================================================ FILE: modules/build/src/main/scala/scala/build/preprocessing/CustomDirectivesReporter.scala ================================================ package scala.build.preprocessing import com.virtuslab.using_directives.custom.utils.Position as DirectivePosition import com.virtuslab.using_directives.reporter.Reporter import scala.build.Position import scala.build.errors.{Diagnostic, Severity} class CustomDirectivesReporter(path: Either[String, os.Path], onDiagnostic: Diagnostic => Unit) extends Reporter { private var errorCount = 0 private var warningCount = 0 private def toScalaCliPosition(position: DirectivePosition): Position = { val coords = (position.getLine, position.getColumn) Position.File(path, coords, coords) } override def error(msg: String): Unit = onDiagnostic { errorCount += 1 Diagnostic(msg, Severity.Error) } override def error(position: DirectivePosition, msg: String): Unit = onDiagnostic { errorCount += 1 Diagnostic(msg, Severity.Error, Seq(toScalaCliPosition(position))) } override def warning(msg: String): Unit = onDiagnostic { warningCount += 1 Diagnostic(msg, Severity.Warning) } override def warning(position: DirectivePosition, msg: String): Unit = onDiagnostic { warningCount += 1 Diagnostic(msg, Severity.Warning, Seq(toScalaCliPosition(position))) } override def hasErrors(): Boolean = errorCount != 0 override def hasWarnings(): Boolean = warningCount != 0 override def reset(): Unit = { errorCount = 0 } } object CustomDirectivesReporter { def create(path: Either[String, os.Path])(onDiagnostic: Diagnostic => Unit) : CustomDirectivesReporter = new CustomDirectivesReporter(path, onDiagnostic) } ================================================ FILE: modules/build/src/main/scala/scala/build/preprocessing/DataPreprocessor.scala ================================================ package scala.build.preprocessing import scala.build.EitherCps.either import scala.build.Logger import scala.build.errors.BuildException import scala.build.input.{ScalaCliInvokeData, SingleElement, VirtualData} import scala.build.options.{BuildRequirements, SuppressWarningOptions} case object DataPreprocessor extends Preprocessor { def preprocess( input: SingleElement, logger: Logger, maybeRecoverOnError: BuildException => Option[BuildException] = e => Some(e), allowRestrictedFeatures: Boolean, suppressWarningOptions: SuppressWarningOptions )(using ScalaCliInvokeData): Option[Either[BuildException, Seq[PreprocessedSource]]] = input match { case file: VirtualData => val res = either { val inMemory = Seq( PreprocessedSource.InMemory( originalPath = Left(file.source), relPath = file.subPath, content = file.content, wrapperParamsOpt = None, options = None, optionsWithTargetRequirements = Nil, requirements = Some(BuildRequirements()), scopedRequirements = Nil, mainClassOpt = None, scopePath = file.scopePath, directivesPositions = None ) ) inMemory } Some(res) case _ => None } } ================================================ FILE: modules/build/src/main/scala/scala/build/preprocessing/DeprecatedDirectives.scala ================================================ package scala.build.preprocessing import scala.build.Logger import scala.build.errors.Diagnostic.TextEdit import scala.build.internal.Constants import scala.build.internal.util.WarningMessages.{deprecatedToolkitLatest, deprecatedWarning} import scala.build.options.SuppressWarningOptions import scala.build.preprocessing.directives.{DirectiveHandler, StrictDirective, Toolkit} import scala.build.warnings.DeprecatedWarning object DeprecatedDirectives { /** Used to represent a general form of a deprecated directive, and its replacement * @param keys * representation of deprecated keys * @param values * representation of deprecated value */ private case class DirectiveTemplate(keys: Seq[String], values: Option[Seq[String]]) { def appliesTo(foundKey: String, foundValues: Seq[String]): Boolean = (keys.isEmpty || keys.contains(foundKey)) && // FIXME values.contains is not perfect, but is enough for now since we don't look for specific multiple values (values.isEmpty || values.contains(foundValues)) } private type WarningAndReplacement = (String, DirectiveTemplate) private def keyReplacement(replacement: String)(warning: String): WarningAndReplacement = (warning, DirectiveTemplate(Seq(replacement), None)) private def valueReplacement(replacements: String*)(warning: String): WarningAndReplacement = (warning, DirectiveTemplate(Nil, Some(replacements.toSeq))) private def allKeysFrom(handler: DirectiveHandler[?]): Seq[String] = handler.keys.flatMap(_.nameAliases) private val deprecatedCombinationsAndReplacements = Map[DirectiveTemplate, WarningAndReplacement]( DirectiveTemplate(Seq("lib"), None) -> keyReplacement("dep")(deprecatedWarning("lib", "dep")), DirectiveTemplate(Seq("libs"), None) -> keyReplacement("dep")(deprecatedWarning( "libs", "deps" )), DirectiveTemplate(Seq("compileOnly.lib"), None) -> keyReplacement("compileOnly.dep")( deprecatedWarning("compileOnly.lib", "compileOnly.dep") ), DirectiveTemplate(Seq("compileOnly.libs"), None) -> keyReplacement("compileOnly.dep")( deprecatedWarning("compileOnly.libs", "compileOnly.deps") ), DirectiveTemplate( allKeysFrom(directives.Toolkit.handler), Some(Seq("latest")) ) -> valueReplacement("default")(deprecatedToolkitLatest()), DirectiveTemplate( allKeysFrom(directives.Toolkit.handler), Some(Seq(s"${Toolkit.typelevel}:latest")) ) -> valueReplacement(s"${Toolkit.typelevel}:default")( deprecatedToolkitLatest() ), DirectiveTemplate( allKeysFrom(directives.Toolkit.handler), Some(Seq(s"${Constants.typelevelOrganization}:latest")) ) -> valueReplacement(s"${Toolkit.typelevel}:default")( deprecatedToolkitLatest() ) ) private def warningAndReplacement(directive: StrictDirective): Option[WarningAndReplacement] = deprecatedCombinationsAndReplacements .find(_._1.appliesTo(directive.key, directive.toStringValues)) .map(_._2) // grab WarningAndReplacement def issueWarnings( path: Either[String, os.Path], directives: Seq[StrictDirective], suppressWarningOptions: SuppressWarningOptions, logger: Logger ): Unit = if !suppressWarningOptions.suppressDeprecatedFeatureWarning.getOrElse(false) then directives.map(d => d -> warningAndReplacement(d)) .foreach { case (directive, Some(warning, replacement)) => val newKey = replacement.keys.headOption.getOrElse(directive.key) val newValues = replacement.values.getOrElse(directive.toStringValues) val newText = s"$newKey ${newValues.mkString(" ")}" // TODO use key and/or value positions instead of whole directive val position = directive.position(path) val diagnostic = DeprecatedWarning( warning, Seq(position), Some(TextEdit(s"Change to: $newText", newText)) ) logger.log(Seq(diagnostic)) case _ => () } } ================================================ FILE: modules/build/src/main/scala/scala/build/preprocessing/DirectivesPreprocessor.scala ================================================ package scala.build.preprocessing import scala.build.EitherCps.{either, value} import scala.build.Logger import scala.build.Ops.* import scala.build.errors.{BuildException, CompositeBuildException, DirectiveErrors} import scala.build.input.ScalaCliInvokeData import scala.build.internal.util.WarningMessages import scala.build.internals.FeatureType import scala.build.options.{ BuildOptions, BuildRequirements, ConfigMonoid, SuppressWarningOptions, WithBuildRequirements } import scala.build.preprocessing.directives.* import scala.build.preprocessing.directives.DirectivesPreprocessingUtils.* case class DirectivesPreprocessor( path: Either[String, os.Path], cwd: ScopePath, logger: Logger, allowRestrictedFeatures: Boolean, suppressWarningOptions: SuppressWarningOptions, maybeRecoverOnError: BuildException => Option[BuildException] )( using ScalaCliInvokeData ) { def preprocess(content: String): Either[BuildException, PreprocessedDirectives] = for { directives <- ExtractedDirectives.from( content.toCharArray, path, suppressWarningOptions, logger, maybeRecoverOnError ) res <- preprocess(directives) } yield res def preprocess(extractedDirectives: ExtractedDirectives) : Either[BuildException, PreprocessedDirectives] = either { val ExtractedDirectives(directives, directivesPositions) = extractedDirectives val ( buildOptionsWithoutRequirements: PartiallyProcessedDirectives[BuildOptions], buildOptionsWithTargetRequirements: PartiallyProcessedDirectives[ List[WithBuildRequirements[BuildOptions]] ], scopedBuildRequirements: PartiallyProcessedDirectives[BuildRequirements], unusedDirectives: Seq[StrictDirective] ) = value { for { regularUsingDirectives: PartiallyProcessedDirectives[BuildOptions] <- applyDirectiveHandlers(directives, usingDirectiveHandlers) usingDirectivesWithRequirements: PartiallyProcessedDirectives[ List[WithBuildRequirements[BuildOptions]] ] <- applyDirectiveHandlers( regularUsingDirectives.unused, usingDirectiveWithReqsHandlers ) targetDirectives: PartiallyProcessedDirectives[BuildRequirements] <- applyDirectiveHandlers( usingDirectivesWithRequirements.unused, requireDirectiveHandlers ) remainingDirectives = targetDirectives.unused } yield ( regularUsingDirectives, usingDirectivesWithRequirements, targetDirectives, remainingDirectives ) } val (optionsWithActualRequirements, optionsWithEmptyRequirements) = buildOptionsWithTargetRequirements.global.partition(_.requirements.nonEmpty) val summedOptionsWithNoRequirements = optionsWithEmptyRequirements .map(_.value) .foldLeft(buildOptionsWithoutRequirements.global)((acc, bo) => acc.orElse(bo)) value { unusedDirectives.toList match { case Nil => Right { PreprocessedDirectives( scopedBuildRequirements.global, summedOptionsWithNoRequirements, optionsWithActualRequirements, scopedBuildRequirements.scoped, strippedContent = None, directivesPositions ) } case unused => maybeRecoverOnError { CompositeBuildException( exceptions = unused.map(ScopedDirective(_, path, cwd).unusedDirectiveError) ) }.toLeft(PreprocessedDirectives.empty) } } } private def applyDirectiveHandlers[T: ConfigMonoid]( directives: Seq[StrictDirective], handlers: Seq[DirectiveHandler[T]] ): Either[BuildException, PartiallyProcessedDirectives[T]] = { val configMonoidInstance = implicitly[ConfigMonoid[T]] val shouldSuppressExperimentalFeatures = suppressWarningOptions.suppressExperimentalFeatureWarning.getOrElse(false) def handleValues(handler: DirectiveHandler[T])( scopedDirective: ScopedDirective, logger: Logger ): Either[BuildException, ProcessedDirective[T]] = if !allowRestrictedFeatures && (handler.isRestricted || handler.isExperimental) then Left(DirectiveErrors( ::(WarningMessages.powerDirectiveUsedInSip(scopedDirective, handler), Nil), Seq(scopedDirective.directive.position(scopedDirective.maybePath)) )) else if handler.isExperimental && !shouldSuppressExperimentalFeatures then logger.experimentalWarning(scopedDirective.directive.toString, FeatureType.Directive) handler.handleValues(scopedDirective, logger) val handlersMap = handlers .flatMap { handler => handler.keys.flatMap(_.nameAliases).map(k => k -> handleValues(handler)) } .toMap val unused = directives.filter(d => !handlersMap.contains(d.key)) val res = directives .iterator .flatMap { case d @ StrictDirective(k, _, _, _) => handlersMap.get(k).iterator.map(_(ScopedDirective(d, path, cwd), logger)) } .toVector .flatMap { case Left(e: BuildException) => maybeRecoverOnError(e).toVector.map(Left(_)) case r @ Right(_) => Vector(r) } .sequence .left.map(CompositeBuildException(_)) .map(_.foldLeft((configMonoidInstance.zero, Seq.empty[Scoped[T]])) { case ((globalAcc, scopedAcc), ProcessedDirective(global, scoped)) => ( global.fold(globalAcc)(ns => configMonoidInstance.orElse(ns, globalAcc)), scopedAcc ++ scoped ) }) res.map { case (g, s) => PartiallyProcessedDirectives(g, s, unused) } } } ================================================ FILE: modules/build/src/main/scala/scala/build/preprocessing/ExtractedDirectives.scala ================================================ package scala.build.preprocessing import com.virtuslab.using_directives.UsingDirectivesProcessor import com.virtuslab.using_directives.custom.model.{BooleanValue, EmptyValue, StringValue, Value} import com.virtuslab.using_directives.custom.utils.ast.* import scala.annotation.targetName import scala.build.errors.* import scala.build.options.SuppressWarningOptions import scala.build.preprocessing.UsingDirectivesOps.* import scala.build.preprocessing.directives.StrictDirective import scala.build.{Logger, Position} import scala.collection.mutable import scala.jdk.CollectionConverters.* case class ExtractedDirectives( directives: Seq[StrictDirective], position: Option[Position.File] ) { @targetName("append") def ++(other: ExtractedDirectives): ExtractedDirectives = ExtractedDirectives(directives ++ other.directives, position) } object ExtractedDirectives { def empty: ExtractedDirectives = ExtractedDirectives(Seq.empty, None) def from( contentChars: Array[Char], path: Either[String, os.Path], suppressWarningOptions: SuppressWarningOptions, logger: Logger, maybeRecoverOnError: BuildException => Option[BuildException] ): Either[BuildException, ExtractedDirectives] = { val errors = new mutable.ListBuffer[Diagnostic] val reporter = CustomDirectivesReporter .create(path) { case diag if diag.severity == Severity.Warning && diag.message.toLowerCase.contains("deprecated") && suppressWarningOptions.suppressDeprecatedFeatureWarning.getOrElse(false) => () // skip deprecated feature warnings if suppressed case diag if diag.severity == Severity.Warning => logger.log(Seq(diag)) case diag => errors += diag } val processor = new UsingDirectivesProcessor(reporter) val allDirectives = processor.extract(contentChars).asScala val malformedDirectiveErrors = errors.map(diag => new MalformedDirectiveError(diag.message, diag.positions)).toSeq val maybeCompositeMalformedDirectiveError = if (malformedDirectiveErrors.nonEmpty) maybeRecoverOnError(CompositeBuildException(malformedDirectiveErrors)) else None if (malformedDirectiveErrors.isEmpty || maybeCompositeMalformedDirectiveError.isEmpty) { val directivesOpt = allDirectives.headOption val directivesPositionOpt = directivesOpt match { case Some(directives) if directives.containsTargetDirectives || directives.isEmpty => None case Some(directives) => Some(directives.getPosition(path)) case None => None } val strictDirectives = directivesOpt.toSeq.flatMap { directives => def toStrictValue(value: UsingValue): Seq[Value[?]] = value match { case uvs: UsingValues => uvs.values.asScala.toSeq.flatMap(toStrictValue) case el: EmptyLiteral => Seq(EmptyValue(el)) case sl: StringLiteral => Seq(StringValue(sl.getValue(), sl)) case bl: BooleanLiteral => Seq(BooleanValue(bl.getValue(), bl)) } def toStrictDirective(ud: UsingDef) = StrictDirective( ud.getKey(), toStrictValue(ud.getValue()), ud.getPosition().getColumn(), ud.getPosition().getLine() ) directives.getAst match case uds: UsingDefs => uds.getUsingDefs.asScala.toSeq.map(toStrictDirective) case ud: UsingDef => Seq(toStrictDirective(ud)) case _ => Nil // There should be nothing else here other than UsingDefs or UsingDef } Right(ExtractedDirectives(strictDirectives.reverse, directivesPositionOpt)) } else maybeCompositeMalformedDirectiveError match { case Some(e) => Left(e) case None => Right(ExtractedDirectives.empty) } } } ================================================ FILE: modules/build/src/main/scala/scala/build/preprocessing/JarPreprocessor.scala ================================================ package scala.build.preprocessing import scala.build.EitherCps.either import scala.build.Logger import scala.build.errors.BuildException import scala.build.input.{JarFile, ScalaCliInvokeData, SingleElement} import scala.build.options.{ BuildOptions, BuildRequirements, ClassPathOptions, SuppressWarningOptions } case object JarPreprocessor extends Preprocessor { def preprocess( input: SingleElement, logger: Logger, maybeRecoverOnError: BuildException => Option[BuildException] = e => Some(e), allowRestrictedFeatures: Boolean, suppressWarningOptions: SuppressWarningOptions )(using ScalaCliInvokeData): Option[Either[BuildException, Seq[PreprocessedSource]]] = input match { case jar: JarFile => Some(either { val buildOptions = BuildOptions().copy( classPathOptions = ClassPathOptions( extraClassPath = Seq(jar.path) ) ) Seq(PreprocessedSource.OnDisk( path = jar.path, options = Some(buildOptions), optionsWithTargetRequirements = List.empty, requirements = Some(BuildRequirements()), scopedRequirements = Nil, mainClassOpt = None, directivesPositions = None )) }) case _ => None } } ================================================ FILE: modules/build/src/main/scala/scala/build/preprocessing/JavaPreprocessor.scala ================================================ package scala.build.preprocessing import coursier.cache.ArchiveCache import coursier.util.Task import java.nio.charset.StandardCharsets import scala.build.EitherCps.{either, value} import scala.build.Logger import scala.build.errors.BuildException import scala.build.input.{JavaFile, ScalaCliInvokeData, SingleElement, VirtualJavaFile} import scala.build.internal.JavaParserProxyMaker import scala.build.options.{BuildRequirements, SuppressWarningOptions} import scala.build.preprocessing.directives.PreprocessedDirectives /** Java source preprocessor. * * Doesn't modify Java sources. This only extracts using directives from them, and for unnamed * sources (like stdin), tries to infer a class name from the sources themselves. * * @param archiveCache * when using a java-class-name external binary to infer a class name (see [[JavaParserProxy]]), * a cache to download that binary with * @param javaClassNameVersionOpt * when using a java-class-name external binary to infer a class name (see [[JavaParserProxy]]), * this forces the java-class-name version to download */ final case class JavaPreprocessor( archiveCache: ArchiveCache[Task], javaClassNameVersionOpt: Option[String], javaCommand: () => String ) extends Preprocessor { def preprocess( input: SingleElement, logger: Logger, maybeRecoverOnError: BuildException => Option[BuildException] = e => Some(e), allowRestrictedFeatures: Boolean, suppressWarningOptions: SuppressWarningOptions )(using ScalaCliInvokeData): Option[Either[BuildException, Seq[PreprocessedSource]]] = input match { case j: JavaFile => Some(either { val content: String = value(PreprocessingUtil.maybeRead(j.path)) val scopePath = ScopePath.fromPath(j.path) val preprocessedDirectives: PreprocessedDirectives = value { DirectivesPreprocessor( Right(j.path), scopePath, logger, allowRestrictedFeatures, suppressWarningOptions, maybeRecoverOnError ) .preprocess(content) } Seq(PreprocessedSource.OnDisk( path = j.path, options = Some(preprocessedDirectives.globalUsings), optionsWithTargetRequirements = preprocessedDirectives.usingsWithReqs, requirements = Some(BuildRequirements()), scopedRequirements = Nil, mainClassOpt = None, directivesPositions = preprocessedDirectives.directivesPositions )) }) case v: VirtualJavaFile => val res = either { val relPath = if (v.isStdin || v.isSnippet) { val classNameOpt = value { (new JavaParserProxyMaker) .get( archiveCache, javaClassNameVersionOpt, logger, () => javaCommand() ) .className(v.content) } val fileName = classNameOpt .map(_ + ".java") .getOrElse(v.generatedSourceFileName) os.sub / fileName } else v.subPath val content = new String(v.content, StandardCharsets.UTF_8) val preprocessedDirectives: PreprocessedDirectives = value { DirectivesPreprocessor( Left(relPath.toString), v.scopePath, logger, allowRestrictedFeatures, suppressWarningOptions, maybeRecoverOnError ).preprocess( content ) } val s = PreprocessedSource.InMemory( originalPath = Left(v.source), relPath = relPath, content = v.content, wrapperParamsOpt = None, options = Some(preprocessedDirectives.globalUsings), optionsWithTargetRequirements = preprocessedDirectives.usingsWithReqs, requirements = Some(BuildRequirements()), scopedRequirements = Nil, mainClassOpt = None, scopePath = v.scopePath, directivesPositions = preprocessedDirectives.directivesPositions ) Seq(s) } Some(res) case _ => None } } ================================================ FILE: modules/build/src/main/scala/scala/build/preprocessing/MarkdownCodeBlockProcessor.scala ================================================ package scala.build.preprocessing import scala.build.EitherCps.{either, value} import scala.build.Logger import scala.build.errors.BuildException import scala.build.internal.markdown.MarkdownCodeBlock import scala.build.options.SuppressWarningOptions object MarkdownCodeBlockProcessor { def process( codeBlocks: Seq[MarkdownCodeBlock], reportingPath: Either[String, os.Path], suppressWarningOptions: SuppressWarningOptions, logger: Logger, maybeRecoverOnError: BuildException => Option[BuildException] ): Either[BuildException, PreprocessedMarkdown] = either { val (rawCodeBlocks, remaining) = codeBlocks.partition(_.isRaw) val (testCodeBlocks, scriptCodeBlocks) = remaining.partition(_.isTest) def preprocessCodeBlocks(cbs: Seq[MarkdownCodeBlock]) : Either[BuildException, PreprocessedMarkdownCodeBlocks] = either { val mergedDirectives: ExtractedDirectives = cbs .map { cb => value { ExtractedDirectives.from( contentChars = cb.body.toCharArray, path = reportingPath, suppressWarningOptions = suppressWarningOptions, logger = logger, maybeRecoverOnError = maybeRecoverOnError ) } } .fold(ExtractedDirectives.empty)(_ ++ _) PreprocessedMarkdownCodeBlocks( cbs, mergedDirectives ) } PreprocessedMarkdown( value(preprocessCodeBlocks(scriptCodeBlocks)), value(preprocessCodeBlocks(rawCodeBlocks)), value(preprocessCodeBlocks(testCodeBlocks)) ) } } ================================================ FILE: modules/build/src/main/scala/scala/build/preprocessing/MarkdownPreprocessor.scala ================================================ package scala.build.preprocessing import java.nio.charset.StandardCharsets import scala.build.EitherCps.{either, value} import scala.build.Logger import scala.build.errors.BuildException import scala.build.input.{MarkdownFile, ScalaCliInvokeData, SingleElement, VirtualMarkdownFile} import scala.build.internal.markdown.{MarkdownCodeBlock, MarkdownCodeWrapper} import scala.build.options.SuppressWarningOptions import scala.build.preprocessing.ScalaPreprocessor.ProcessingOutput case object MarkdownPreprocessor extends Preprocessor { def preprocess( input: SingleElement, logger: Logger, maybeRecoverOnError: BuildException => Option[BuildException], allowRestrictedFeatures: Boolean, suppressWarningOptions: SuppressWarningOptions )(using ScalaCliInvokeData): Option[Either[BuildException, Seq[PreprocessedSource]]] = input match { case markdown: MarkdownFile => val res = either { val content = value(PreprocessingUtil.maybeRead(markdown.path)) val preprocessed = value { MarkdownPreprocessor.preprocess( Right(markdown.path), content, markdown.subPath, ScopePath.fromPath(markdown.path), logger, maybeRecoverOnError, allowRestrictedFeatures, suppressWarningOptions ) } preprocessed } Some(res) case markdown: VirtualMarkdownFile => val content = new String(markdown.content, StandardCharsets.UTF_8) val res = either { val preprocessed = value { MarkdownPreprocessor.preprocess( Left(markdown.source), content, markdown.wrapperPath, markdown.scopePath, logger, maybeRecoverOnError, allowRestrictedFeatures, suppressWarningOptions ) } preprocessed } Some(res) case _ => None } private def preprocess( reportingPath: Either[String, os.Path], content: String, subPath: os.SubPath, scopePath: ScopePath, logger: Logger, maybeRecoverOnError: BuildException => Option[BuildException], allowRestrictedFeatures: Boolean, suppressWarningOptions: SuppressWarningOptions )(using ScalaCliInvokeData): Either[BuildException, List[PreprocessedSource.InMemory]] = either { def preprocessSnippets( maybeWrapper: Option[MarkdownCodeWrapper.WrappedMarkdownCode], generatedSourceNameSuffix: String ): Either[BuildException, Option[PreprocessedSource.InMemory]] = either { maybeWrapper .map { wrappedMarkdown => val processingOutput: ProcessingOutput = value { ScalaPreprocessor.processSources( content = wrappedMarkdown.code, extractedDirectives = wrappedMarkdown.directives, path = reportingPath, scopeRoot = scopePath / os.up, logger = logger, allowRestrictedFeatures = allowRestrictedFeatures, suppressWarningOptions = suppressWarningOptions, maybeRecoverOnError = maybeRecoverOnError ) }.getOrElse(ProcessingOutput.empty) val processedCode = processingOutput.updatedContent.getOrElse(wrappedMarkdown.code) PreprocessedSource.InMemory( originalPath = reportingPath.map(subPath -> _), relPath = os.rel / (subPath / os.up) / s"${subPath.last}$generatedSourceNameSuffix", processedCode.getBytes(StandardCharsets.UTF_8), wrapperParamsOpt = None, options = Some(processingOutput.opts), optionsWithTargetRequirements = processingOutput.optsWithReqs, requirements = Some(processingOutput.globalReqs), processingOutput.scopedReqs, mainClassOpt = None, scopePath = scopePath, directivesPositions = processingOutput.directivesPositions ) } } val codeBlocks: Seq[MarkdownCodeBlock] = value(MarkdownCodeBlock.findCodeBlocks(subPath, content, maybeRecoverOnError)) val preprocessedMarkdown: PreprocessedMarkdown = value(MarkdownCodeBlockProcessor.process( codeBlocks, reportingPath, suppressWarningOptions, logger, maybeRecoverOnError )) val (mainScalaCode, rawScalaCode, testScalaCode) = MarkdownCodeWrapper(subPath, preprocessedMarkdown) val maybeMainFile = value(preprocessSnippets(mainScalaCode, ".scala")) val maybeRawFile = value(preprocessSnippets(rawScalaCode, ".raw.scala")) val maybeTestFile = value(preprocessSnippets(testScalaCode, ".test.scala")) maybeMainFile.toList ++ maybeTestFile ++ maybeRawFile } } ================================================ FILE: modules/build/src/main/scala/scala/build/preprocessing/PreprocessedMarkdown.scala ================================================ package scala.build.preprocessing import scala.build.internal.markdown.MarkdownCodeBlock case class PreprocessedMarkdownCodeBlocks( codeBlocks: Seq[MarkdownCodeBlock], extractedDirectives: ExtractedDirectives = ExtractedDirectives.empty ) object PreprocessedMarkdownCodeBlocks { def empty: PreprocessedMarkdownCodeBlocks = PreprocessedMarkdownCodeBlocks(Seq.empty, ExtractedDirectives.empty) } case class PreprocessedMarkdown( scriptCodeBlocks: PreprocessedMarkdownCodeBlocks = PreprocessedMarkdownCodeBlocks.empty, rawCodeBlocks: PreprocessedMarkdownCodeBlocks = PreprocessedMarkdownCodeBlocks.empty, testCodeBlocks: PreprocessedMarkdownCodeBlocks = PreprocessedMarkdownCodeBlocks.empty ) object PreprocessedMarkdown { def empty: PreprocessedMarkdown = PreprocessedMarkdown() } ================================================ FILE: modules/build/src/main/scala/scala/build/preprocessing/PreprocessedSource.scala ================================================ package scala.build.preprocessing import scala.build.Position import scala.build.internal.{CodeWrapper, WrapperParams} import scala.build.options.{BuildOptions, BuildRequirements, WithBuildRequirements} sealed abstract class PreprocessedSource extends Product with Serializable { def options: Option[BuildOptions] def optionsWithTargetRequirements: List[WithBuildRequirements[BuildOptions]] def requirements: Option[BuildRequirements] def mainClassOpt: Option[String] def scopedRequirements: Seq[Scoped[BuildRequirements]] def scopePath: ScopePath def directivesPositions: Option[Position.File] def distinctPathOrSource: String = this match { case p: PreprocessedSource.OnDisk => p.path.toString case p: PreprocessedSource.InMemory => s"${p.originalPath}; ${p.relPath}" case p: PreprocessedSource.UnwrappedScript => p.originalPath.toString case p: PreprocessedSource.NoSourceCode => p.path.toString } } object PreprocessedSource { final case class OnDisk( path: os.Path, options: Option[BuildOptions], optionsWithTargetRequirements: List[WithBuildRequirements[BuildOptions]], requirements: Option[BuildRequirements], scopedRequirements: Seq[Scoped[BuildRequirements]], mainClassOpt: Option[String], directivesPositions: Option[Position.File] ) extends PreprocessedSource { def scopePath: ScopePath = ScopePath.fromPath(path) } final case class InMemory( originalPath: Either[String, (os.SubPath, os.Path)], relPath: os.RelPath, content: Array[Byte], wrapperParamsOpt: Option[WrapperParams], options: Option[BuildOptions], optionsWithTargetRequirements: List[WithBuildRequirements[BuildOptions]], requirements: Option[BuildRequirements], scopedRequirements: Seq[Scoped[BuildRequirements]], mainClassOpt: Option[String], scopePath: ScopePath, directivesPositions: Option[Position.File] ) extends PreprocessedSource { def reportingPath: Either[String, os.Path] = originalPath.map(_._2) } final case class UnwrappedScript( originalPath: Either[String, (os.SubPath, os.Path)], relPath: os.RelPath, options: Option[BuildOptions], optionsWithTargetRequirements: List[WithBuildRequirements[BuildOptions]], requirements: Option[BuildRequirements], scopedRequirements: Seq[Scoped[BuildRequirements]], scopePath: ScopePath, directivesPositions: Option[Position.File], wrapScriptFun: CodeWrapper => (String, WrapperParams) ) extends PreprocessedSource { override def mainClassOpt: Option[String] = None } final case class NoSourceCode( options: Option[BuildOptions], optionsWithTargetRequirements: List[WithBuildRequirements[BuildOptions]], requirements: Option[BuildRequirements], scopedRequirements: Seq[Scoped[BuildRequirements]], path: os.Path ) extends PreprocessedSource { def mainClassOpt: None.type = None def scopePath: ScopePath = ScopePath.fromPath(path) def directivesPositions: None.type = None } } ================================================ FILE: modules/build/src/main/scala/scala/build/preprocessing/PreprocessingUtil.scala ================================================ package scala.build.preprocessing import java.nio.charset.StandardCharsets import scala.build.errors.{BuildException, FileNotFoundException} object PreprocessingUtil { private def defaultCharSet = StandardCharsets.UTF_8 def maybeRead(f: os.Path): Either[BuildException, String] = if (os.isFile(f)) Right(os.read(f, defaultCharSet)) else Left(new FileNotFoundException(f)) } ================================================ FILE: modules/build/src/main/scala/scala/build/preprocessing/Preprocessor.scala ================================================ package scala.build.preprocessing import scala.build.Logger import scala.build.errors.BuildException import scala.build.input.{ScalaCliInvokeData, SingleElement} import scala.build.options.SuppressWarningOptions trait Preprocessor { def preprocess( input: SingleElement, logger: Logger, maybeRecoverOnError: BuildException => Option[BuildException] = e => Some(e), allowRestrictedFeatures: Boolean, suppressWarningOptions: SuppressWarningOptions )(using ScalaCliInvokeData): Option[Either[BuildException, Seq[PreprocessedSource]]] } ================================================ FILE: modules/build/src/main/scala/scala/build/preprocessing/ScalaPreprocessor.scala ================================================ package scala.build.preprocessing import java.nio.charset.StandardCharsets import scala.build.EitherCps.{either, value} import scala.build.errors.* import scala.build.input.{ScalaCliInvokeData, ScalaFile, SingleElement, VirtualScalaFile} import scala.build.options.* import scala.build.preprocessing.directives.PreprocessedDirectives import scala.build.{Logger, Position} case object ScalaPreprocessor extends Preprocessor { case class ProcessingOutput( globalReqs: BuildRequirements, scopedReqs: Seq[Scoped[BuildRequirements]], opts: BuildOptions, optsWithReqs: List[WithBuildRequirements[BuildOptions]], updatedContent: Option[String], directivesPositions: Option[Position.File] ) object ProcessingOutput { def empty: ProcessingOutput = ProcessingOutput( BuildRequirements(), Nil, BuildOptions(), List.empty, None, None ) } def preprocess( input: SingleElement, logger: Logger, maybeRecoverOnError: BuildException => Option[BuildException] = e => Some(e), allowRestrictedFeatures: Boolean, suppressWarningOptions: SuppressWarningOptions )(using ScalaCliInvokeData): Option[Either[BuildException, Seq[PreprocessedSource]]] = input match { case f: ScalaFile => val res = either { val content = value(PreprocessingUtil.maybeRead(f.path)) val scopePath = ScopePath.fromPath(f.path) val source = value( process( content, Right(f.path), scopePath / os.up, logger, maybeRecoverOnError, allowRestrictedFeatures, suppressWarningOptions ) ) match { case None => PreprocessedSource.OnDisk(f.path, None, List.empty, None, Nil, None, None) case Some(ProcessingOutput( requirements, scopedRequirements, options, optionsWithReqs, Some(updatedCode), directivesPositions )) => PreprocessedSource.InMemory( originalPath = Right((f.subPath, f.path)), relPath = f.subPath, content = updatedCode.getBytes(StandardCharsets.UTF_8), wrapperParamsOpt = None, options = Some(options), optionsWithTargetRequirements = optionsWithReqs, requirements = Some(requirements), scopedRequirements = scopedRequirements, mainClassOpt = None, scopePath = scopePath, directivesPositions = directivesPositions ) case Some(ProcessingOutput( requirements, scopedRequirements, options, optionsWithReqs, None, directivesPositions )) => PreprocessedSource.OnDisk( path = f.path, options = Some(options), optionsWithTargetRequirements = optionsWithReqs, requirements = Some(requirements), scopedRequirements = scopedRequirements, mainClassOpt = None, directivesPositions = directivesPositions ) } Seq(source) } Some(res) case v: VirtualScalaFile => val res = either { val relPath = v match { case v if !v.isStdin && !v.isSnippet => v.subPath case v => os.sub / v.generatedSourceFileName } val content = new String(v.content, StandardCharsets.UTF_8) val ( requirements: BuildRequirements, scopedRequirements: Seq[Scoped[BuildRequirements]], options: BuildOptions, optionsWithTargetRequirements: List[WithBuildRequirements[BuildOptions]], updatedContentOpt: Option[String], directivesPositions: Option[Position.File] ) = value( process( content, Left(v.source), v.scopePath / os.up, logger, maybeRecoverOnError, allowRestrictedFeatures, suppressWarningOptions ) ).map { case ProcessingOutput( reqs, scopedReqs, opts, optsWithReqs, updatedContent, dirsPositions ) => (reqs, scopedReqs, opts, optsWithReqs, updatedContent, dirsPositions) }.getOrElse(( BuildRequirements(), Nil, BuildOptions(), List(WithBuildRequirements(BuildRequirements(), BuildOptions())), None, None )) val s = PreprocessedSource.InMemory( originalPath = Left(v.source), relPath = relPath, updatedContentOpt.map(_.getBytes(StandardCharsets.UTF_8)).getOrElse(v.content), wrapperParamsOpt = None, options = Some(options), optionsWithTargetRequirements = optionsWithTargetRequirements, requirements = Some(requirements), scopedRequirements, mainClassOpt = None, scopePath = v.scopePath, directivesPositions = directivesPositions ) Seq(s) } Some(res) case _ => None } def process( content: String, path: Either[String, os.Path], scopeRoot: ScopePath, logger: Logger, maybeRecoverOnError: BuildException => Option[BuildException], allowRestrictedFeatures: Boolean, suppressWarningOptions: SuppressWarningOptions )(using ScalaCliInvokeData): Either[BuildException, Option[ProcessingOutput]] = either { val (contentWithNoShebang, _, _) = SheBang.ignoreSheBangLines(content) val extractedDirectives: ExtractedDirectives = value(ExtractedDirectives.from( contentChars = contentWithNoShebang.toCharArray, path = path, suppressWarningOptions = suppressWarningOptions, logger = logger, maybeRecoverOnError = maybeRecoverOnError )) value { processSources( content, extractedDirectives, path, scopeRoot, logger, allowRestrictedFeatures, suppressWarningOptions, maybeRecoverOnError ) } } def processSources( content: String, extractedDirectives: ExtractedDirectives, path: Either[String, os.Path], scopeRoot: ScopePath, logger: Logger, allowRestrictedFeatures: Boolean, suppressWarningOptions: SuppressWarningOptions, maybeRecoverOnError: BuildException => Option[BuildException] )(using ScalaCliInvokeData): Either[BuildException, Option[ProcessingOutput]] = either { DeprecatedDirectives.issueWarnings( path, extractedDirectives.directives, suppressWarningOptions, logger ) val (content0, isSheBang, _) = SheBang.ignoreSheBangLines(content) val preprocessedDirectives: PreprocessedDirectives = value(DirectivesPreprocessor( path, scopeRoot, logger, allowRestrictedFeatures, suppressWarningOptions, maybeRecoverOnError ).preprocess( extractedDirectives )) if (preprocessedDirectives.isEmpty) None else { val allRequirements = Seq(preprocessedDirectives.globalReqs) val summedRequirements = allRequirements.foldLeft(BuildRequirements())(_.orElse(_)) val allOptions = Seq(preprocessedDirectives.globalUsings) val summedOptions = allOptions.foldLeft(BuildOptions())(_.orElse(_)) val lastContentOpt = preprocessedDirectives.strippedContent .orElse(if (isSheBang) Some(content0) else None) val directivesPositions = preprocessedDirectives.directivesPositions.map { pos => if (isSheBang) pos.copy(endPos = pos.endPos._1 + 1 -> pos.endPos._2) else pos } val scopedRequirements = preprocessedDirectives.scopedReqs Some(ProcessingOutput( summedRequirements, scopedRequirements, summedOptions, preprocessedDirectives.usingsWithReqs, lastContentOpt, directivesPositions )) } } } ================================================ FILE: modules/build/src/main/scala/scala/build/preprocessing/ScriptPreprocessor.scala ================================================ package scala.build.preprocessing import java.nio.charset.StandardCharsets import scala.build.EitherCps.{either, value} import scala.build.Logger import scala.build.errors.BuildException import scala.build.input.{ScalaCliInvokeData, Script, SingleElement, VirtualScript} import scala.build.internal.* import scala.build.internal.util.WarningMessages import scala.build.options.{BuildOptions, Platform, SuppressWarningOptions} import scala.build.preprocessing.ScalaPreprocessor.ProcessingOutput case object ScriptPreprocessor extends Preprocessor { def preprocess( input: SingleElement, logger: Logger, maybeRecoverOnError: BuildException => Option[BuildException] = e => Some(e), allowRestrictedFeatures: Boolean, suppressWarningOptions: SuppressWarningOptions )(using ScalaCliInvokeData): Option[Either[BuildException, Seq[PreprocessedSource]]] = input match { case script: Script => val res = either { val content = value(PreprocessingUtil.maybeRead(script.path)) val preprocessed = value { ScriptPreprocessor.preprocess( Right(script.path), content, script.subPath, script.inputArg, ScopePath.fromPath(script.path), logger, maybeRecoverOnError, allowRestrictedFeatures, suppressWarningOptions ) } preprocessed } Some(res) case script: VirtualScript => val content = new String(script.content, StandardCharsets.UTF_8) val res = either { val preprocessed = value { ScriptPreprocessor.preprocess( Left(script.source), content, script.wrapperPath, None, script.scopePath, logger, maybeRecoverOnError, allowRestrictedFeatures, suppressWarningOptions ) } preprocessed } Some(res) case _ => None } private def preprocess( reportingPath: Either[String, os.Path], content: String, subPath: os.SubPath, inputArgPath: Option[String], scopePath: ScopePath, logger: Logger, maybeRecoverOnError: BuildException => Option[BuildException], allowRestrictedFeatures: Boolean, suppressWarningOptions: SuppressWarningOptions )(using ScalaCliInvokeData): Either[BuildException, List[PreprocessedSource.UnwrappedScript]] = either { val (pkg, wrapper) = AmmUtil.pathToPackageWrapper(subPath) val processingOutput: ProcessingOutput = value(ScalaPreprocessor.process( content, reportingPath, scopePath / os.up, logger, maybeRecoverOnError, allowRestrictedFeatures, suppressWarningOptions )) .getOrElse(ProcessingOutput.empty) val scriptCode = processingOutput.updatedContent.getOrElse(SheBang.ignoreSheBangLines(content)._1) // try to match in multiline mode, don't match comment lines starting with '//' val containsMainAnnot = "(?m)^(?!//).*@main.*".r.findFirstIn(scriptCode).isDefined val wrapScriptFun = getScriptWrappingFunction( logger, containsMainAnnot, pkg, wrapper, scriptCode, inputArgPath.getOrElse(subPath.toString) ) (pkg :+ wrapper).map(_.raw).mkString(".") val relPath = os.rel / (subPath / os.up) / s"${subPath.last.stripSuffix(".sc")}.scala" val file = PreprocessedSource.UnwrappedScript( originalPath = reportingPath.map((subPath, _)), relPath = relPath, options = Some(processingOutput.opts), optionsWithTargetRequirements = processingOutput.optsWithReqs, requirements = Some(processingOutput.globalReqs), scopedRequirements = processingOutput.scopedReqs, scopePath = scopePath, directivesPositions = processingOutput.directivesPositions, wrapScriptFun = wrapScriptFun ) List(file) } def getScriptWrappingFunction( logger: Logger, containsMainAnnot: Boolean, packageStrings: Seq[Name], wrapperName: Name, scriptCode: String, scriptPath: String ): CodeWrapper => (String, WrapperParams) = { (codeWrapper: CodeWrapper) => if (containsMainAnnot) logger.diagnostic( codeWrapper match { case _: AppCodeWrapper => WarningMessages.mainAnnotationNotSupported( /* annotationIgnored */ true) case _ => WarningMessages.mainAnnotationNotSupported( /* annotationIgnored */ false) } ) val (code, wrapperParams) = codeWrapper.wrapCode( packageStrings, wrapperName, scriptCode, scriptPath ) (code, wrapperParams) } /** Get correct script wrapper depending on the platform and version of Scala. For Scala 2 or * Platform JS use [[ObjectCodeWrapper]]. Otherwise - for Scala 3 on JVM or Native use * [[ClassCodeWrapper]]. * @param buildOptions * final version of options, build may fail if incompatible wrapper is chosen * @return * code wrapper compatible with provided BuildOptions */ def getScriptWrapper(buildOptions: BuildOptions, logger: Logger): CodeWrapper = { val effectiveScalaVersion = buildOptions.scalaOptions.scalaVersion.flatMap(_.versionOpt) .orElse(buildOptions.scalaOptions.defaultScalaVersion) .getOrElse(Constants.defaultScalaVersion) def logWarning(msg: String) = logger.diagnostic(msg) def objectCodeWrapperForScalaVersion = // AppObjectWrapper only introduces the 'main.sc' restriction when used in Scala 3, there's no gain in using it with Scala 3 if effectiveScalaVersion.startsWith("2") then AppCodeWrapper(effectiveScalaVersion, logWarning) else ObjectCodeWrapper(effectiveScalaVersion, logWarning) buildOptions.scriptOptions.forceObjectWrapper match { case Some(true) => objectCodeWrapperForScalaVersion case _ => buildOptions.scalaOptions.platform.map(_.value) match { case Some(_: Platform.JS.type) => objectCodeWrapperForScalaVersion case _ if effectiveScalaVersion.startsWith("2") => AppCodeWrapper(effectiveScalaVersion, logWarning) case _ => ClassCodeWrapper(effectiveScalaVersion, logWarning) } } } } ================================================ FILE: modules/build/src/main/scala/scala/build/preprocessing/SheBang.scala ================================================ package scala.build.preprocessing object SheBang { def isShebangScript(content: String): Boolean = content.startsWith("#!") def partitionOnShebangSection(content: String): (String, String, String) = if (content.startsWith("#!")) { val splitIndex = content.indexOf("\n!#") match { case -1 => val eolIndex = content.indexOf("\n") content.drop(eolIndex + 1) match { case s if s.startsWith("#!") => eolIndex + s.indexOf("\n") + 1 // skip over #! nix-shell line case _ => eolIndex + 1 } case index => var i = index + 1 while (i < content.length && content(i) != '\n') i += 1 i + 1 // split at start of subsequent line } val newLine: String = content.drop(splitIndex - 2).take(2) match { case CRLF => CRLF case _ => "\n" } val (header, body) = content.splitAt(splitIndex) (header, body, newLine) } else ("", content, lineSeparator(content)) def ignoreSheBangLines(content: String): (String, Boolean, String) = if (content.startsWith("#!")) val (header, body, newLine) = partitionOnShebangSection(content) val blankHeader = newLine * (header.split("\\R", -1).length - 1) (s"$blankHeader$body", true, newLine) else (content, false, lineSeparator(content)) def lineSeparator(content: String): String = content.indexOf(CRLF) match { case -1 => "\n" case _ => CRLF } private final val CRLF: String = "\r\n" } ================================================ FILE: modules/build/src/main/scala/scala/build/preprocessing/UsingDirectivesOps.scala ================================================ package scala.build.preprocessing import com.virtuslab.using_directives.custom.model.UsingDirectives import com.virtuslab.using_directives.custom.utils.ast.* import scala.annotation.tailrec import scala.build.Position import scala.jdk.CollectionConverters.* object UsingDirectivesOps { extension (ud: UsingDirectives) { def keySet: Set[String] = ud.getFlattenedMap.keySet().asScala.map(_.toString).toSet def containsTargetDirectives: Boolean = ud.keySet.exists(_.startsWith("target.")) def getPosition(path: Either[String, os.Path]): Position.File = extension (pos: Positioned) { def getLine = pos.getPosition.getLine def getColumn = pos.getPosition.getColumn } @tailrec def getEndPostion(ast: UsingTree): (Int, Int) = ast match { case uds: UsingDefs => uds.getUsingDefs.asScala match { case _ :+ lastUsingDef => getEndPostion(lastUsingDef) case _ => (uds.getLine, uds.getColumn) } case ud: UsingDef => getEndPostion(ud.getValue) case uvs: UsingValues => uvs.getValues.asScala match { case _ :+ lastUsingValue => getEndPostion(lastUsingValue) case _ => (uvs.getLine, uvs.getColumn) } case sl: StringLiteral => ( sl.getLine, sl.getColumn + sl.getValue.length + { if sl.getIsWrappedDoubleQuotes then 2 else 0 } ) case bl: BooleanLiteral => (bl.getLine, bl.getColumn + bl.getValue.toString.length) case el: EmptyLiteral => (el.getLine, el.getColumn) } val (line, column) = getEndPostion(ud.getAst) Position.File(path, (0, 0), (line, column), ud.getCodeOffset) def getDirectives = ud.getAst match { case usingDefs: UsingDefs => usingDefs.getUsingDefs.asScala.toSeq case _ => Nil } def nonEmpty: Boolean = !isEmpty def isEmpty: Boolean = ud.getFlattenedMap.isEmpty } } ================================================ FILE: modules/build/src/main/scala/scala/build/preprocessing/directives/DirectivesPreprocessingUtils.scala ================================================ package scala.build.preprocessing.directives import scala.build.directives.{ HasBuildOptions, HasBuildOptionsWithRequirements, HasBuildRequirements } import scala.build.options.{BuildOptions, BuildRequirements, WithBuildRequirements} import scala.build.preprocessing.directives object DirectivesPreprocessingUtils { val usingDirectiveHandlers: Seq[DirectiveHandler[BuildOptions]] = Seq[DirectiveHandler[? <: HasBuildOptions]]( directives.Benchmarking.handler, directives.BuildInfo.handler, directives.ComputeVersion.handler, directives.Exclude.handler, directives.JavaHome.handler, directives.Jvm.handler, directives.MainClass.handler, directives.ObjectWrapper.handler, directives.Packaging.handler, directives.Platform.handler, directives.Plugin.handler, directives.Publish.handler, directives.PublishContextual.Local.handler, directives.PublishContextual.CI.handler, directives.Python.handler, directives.Repository.handler, directives.ScalaJs.handler, directives.ScalaNative.handler, directives.ScalaVersion.handler, directives.Sources.handler, directives.Watching.handler, directives.Tests.handler ).map(_.mapE(_.buildOptions)) val usingDirectiveWithReqsHandlers : Seq[DirectiveHandler[List[WithBuildRequirements[BuildOptions]]]] = Seq[DirectiveHandler[? <: HasBuildOptionsWithRequirements]]( directives.CustomJar.handler, directives.Dependency.handler, directives.JavaOptions.handler, directives.JavacOptions.handler, directives.JavaProps.handler, directives.Resources.handler, directives.ScalacOptions.handler, directives.Toolkit.handler ).map(_.mapE(_.buildOptionsWithRequirements)) val requireDirectiveHandlers: Seq[DirectiveHandler[BuildRequirements]] = Seq[DirectiveHandler[? <: HasBuildRequirements]]( directives.RequirePlatform.handler, directives.RequireScalaVersion.handler, directives.RequireScalaVersionBounds.handler, directives.RequireScope.handler ).map(_.mapE(_.buildRequirements)) } ================================================ FILE: modules/build/src/main/scala/scala/build/preprocessing/directives/PartiallyProcessedDirectives.scala ================================================ package scala.build.preprocessing.directives import scala.build.preprocessing.Scoped case class PartiallyProcessedDirectives[T]( global: T, scoped: Seq[Scoped[T]], unused: Seq[StrictDirective] ) ================================================ FILE: modules/build/src/main/scala/scala/build/preprocessing/directives/PreprocessedDirectives.scala ================================================ package scala.build.preprocessing.directives import scala.build.Position import scala.build.options.{BuildOptions, BuildRequirements, WithBuildRequirements} import scala.build.preprocessing.Scoped case class PreprocessedDirectives( globalReqs: BuildRequirements, globalUsings: BuildOptions, usingsWithReqs: List[WithBuildRequirements[BuildOptions]], scopedReqs: Seq[Scoped[BuildRequirements]], strippedContent: Option[String], directivesPositions: Option[Position.File] ) { def isEmpty: Boolean = globalReqs == BuildRequirements.monoid.zero && globalUsings == BuildOptions.monoid.zero && scopedReqs.isEmpty && strippedContent.isEmpty && usingsWithReqs.isEmpty } object PreprocessedDirectives { def empty: PreprocessedDirectives = PreprocessedDirectives( globalReqs = BuildRequirements.monoid.zero, globalUsings = BuildOptions.monoid.zero, usingsWithReqs = Nil, scopedReqs = Nil, strippedContent = None, directivesPositions = None ) } ================================================ FILE: modules/build/src/test/scala/scala/build/options/publish/ComputeVersionTests.scala ================================================ package scala.build.options.publish import com.eed3si9n.expecty.Expecty.expect import org.eclipse.jgit.api.Git import org.eclipse.jgit.lib.Constants import scala.build.options.ComputeVersion import scala.build.tests.{TestInputs, TestUtil} class ComputeVersionTests extends munit.FunSuite { test("git tag") { TestInputs().fromRoot { root => val ghRepo = "scala-cli/compute-version-test" val repo = if (TestUtil.isCI) s"https://git@github.com/$ghRepo.git" else s"https://github.com/$ghRepo.git" os.proc("git", "clone", repo) .call(cwd = root, stdin = os.Inherit, stdout = os.Inherit) val dir = root / "compute-version-test" val cv = ComputeVersion.GitTag(os.rel, true, Nil, "0.0.1-SNAPSHOT") val commitExpectedVersions = Seq( "8ea4e87f202fbcc369bec9615e7ddf2c14b39e9d" -> "0.2.0-1-g8ea4e87-SNAPSHOT", "v0.2.0" -> "0.2.0", "698893f0a4cb1e758cbc8f748827daaf6c7b36d0" -> "0.0.1-SNAPSHOT" ) for ((commit, expectedVersion) <- commitExpectedVersions) { os.proc("git", "checkout", commit) .call(cwd = dir, stdin = os.Inherit, stdout = os.Inherit) val version = cv.get(dir) .fold(ex => throw new Exception(ex), identity) expect(version == expectedVersion) } } } test("git tag on empty repo") { TestInputs().fromRoot { root => val git = Git.init().setDirectory(root.toIO).call() val hasHead = git.getRepository.resolve(Constants.HEAD) != null expect(!hasHead) val defaultVersion = "0.0.2-SNAPSHOT" val cv = ComputeVersion.GitTag(os.rel, true, Nil, defaultVersion) val version = cv.get(root) .fold(ex => throw new Exception(ex), identity) expect(version == defaultVersion) } } } ================================================ FILE: modules/build/src/test/scala/scala/build/options/publish/VcsParseTest.scala ================================================ package scala.build.options.publish import scala.build.Positioned import scala.build.errors.{BuildException, MalformedInputError} class VcsParseTest extends munit.FunSuite { test("valid GitHub") { val actual = Vcs.parse(Positioned.none("github:VirtusLab/scala-cli")) val expected: Either[BuildException, Vcs] = Right(Vcs( "https://github.com/VirtusLab/scala-cli.git", "scm:git:github.com/VirtusLab/scala-cli.git", "scm:git:git@github.com:VirtusLab/scala-cli.git" )) assertEquals(expected, actual) } test("invalid GitHub: missing /") { val actual: Either[BuildException, Vcs] = Vcs.parse(Positioned.none("github:scala-cli")) val expected = Left(new MalformedInputError("github-vcs", "github:scala-cli", "github:org/project", Nil)) assert { actual match { case Left(_: BuildException) => true case _ => sys.error("incorrect type") } } assertEquals(expected.toString, actual.toString) } test("invalid GitHub: too many /") { val actual: Either[BuildException, Vcs] = Vcs.parse(Positioned.none("github:github.com/VirtusLab/scala-cli")) val expected = Left(new MalformedInputError( "github-vcs", "github:github.com/VirtusLab/scala-cli", "github:org/project", Nil )) assert { actual match { case Left(_: BuildException) => true case _ => sys.error("incorrect type") } } assertEquals(expected.toString, actual.toString) } test("valid generic") { val actual = Vcs.parse(Positioned.none( "https://github.com/VirtusLab/scala-cli.git|scm:git:github.com/VirtusLab/scala-cli.git|scm:git:git@github.com:VirtusLab/scala-cli.git" )) val expected: Either[BuildException, Vcs] = Right(Vcs( "https://github.com/VirtusLab/scala-cli.git", "scm:git:github.com/VirtusLab/scala-cli.git", "scm:git:git@github.com:VirtusLab/scala-cli.git" )) assertEquals(expected, actual) } test("invalid generic: missing |") { val actual: Either[BuildException, Vcs] = Vcs.parse(Positioned.none( "https://github.com/VirtusLab/scala-cli|scm:git:github.com/VirtusLab/scala-cli.git" )) val expected = Left(new MalformedInputError( "vcs", "https://github.com/VirtusLab/scala-cli|scm:git:github.com/VirtusLab/scala-cli.git", "url|connection|developer-connection", Nil )) assert { actual match { case Left(_: BuildException) => true case _ => sys.error("incorrect type") } } assertEquals(expected.toString, actual.toString) } test("invalid generic: extra |") { val actual: Either[BuildException, Vcs] = Vcs.parse(Positioned.none("a|b|c|d")) val expected = Left(new MalformedInputError("vcs", "a|b|c|d", "url|connection|developer-connection", Nil)) assert { actual match { case Left(_: BuildException) => true case _ => sys.error("incorrect type") } } assertEquals(expected.toString, actual.toString) } test("invalid generic: gibberish") { val actual: Either[BuildException, Vcs] = Vcs.parse(Positioned.none("sfrgt pagdhn")) val expected = Left(new MalformedInputError( "vcs", "sfrgt pagdhn", "url|connection|developer-connection", Nil )) assert { actual match { case Left(_: BuildException) => true case _ => sys.error("incorrect type") } } assertEquals(expected.toString, actual.toString) } } ================================================ FILE: modules/build/src/test/scala/scala/build/tests/ActionableDiagnosticTests.scala ================================================ package scala.build.tests import com.eed3si9n.expecty.Expecty.expect import coursier.version.Version import scala.build.Ops.* import scala.build.Position.File import scala.build.actionable.ActionableDiagnostic.* import scala.build.actionable.ActionablePreprocessor import scala.build.options.{BuildOptions, InternalOptions, SuppressWarningOptions} import scala.build.{BuildThreads, Directories, LocalRepo} class ActionableDiagnosticTests extends TestUtil.ScalaCliBuildSuite { val extraRepoTmpDir: os.Path = os.temp.dir(prefix = "scala-cli-tests-actionable-diagnostic-") val directories: Directories = Directories.under(extraRepoTmpDir) val baseOptions = BuildOptions( internal = InternalOptions( localRepository = LocalRepo.localRepo(directories.localRepoDir, TestLogger()) ) ) val buildThreads: BuildThreads = BuildThreads.create() def path2url(p: os.Path): String = p.toIO.toURI.toURL.toString test("using outdated os-lib") { val dependencyOsLib = "com.lihaoyi::os-lib:0.7.8" val testInputs = TestInputs( os.rel / "Foo.scala" -> s"""//> using dep $dependencyOsLib | |object Hello extends App { | println("Hello") |} |""".stripMargin ) testInputs.withBuild(baseOptions, buildThreads, None, actionableDiagnostics = true) { (_, _, maybeBuild) => val build = maybeBuild.orThrow val updateDiagnostics = ActionablePreprocessor.generateActionableDiagnostics(build.options).orThrow val osLibDiagnosticOpt = updateDiagnostics.collectFirst { case diagnostic: ActionableDependencyUpdateDiagnostic => diagnostic } expect(osLibDiagnosticOpt.nonEmpty) val osLibDiagnostic = osLibDiagnosticOpt.get expect(Version(osLibDiagnostic.newVersion) > Version(osLibDiagnostic.currentVersion)) } } test("actionable diagnostic report correct position") { val dependencyOsLib = "com.lihaoyi::os-lib:0.7.8" val dependencyPprintLib = "com.lihaoyi::pprint:0.6.6" val testInputs = TestInputs( os.rel / "Foo.scala" -> s"""//> using dep $dependencyOsLib |//> using dep $dependencyPprintLib | |object Hello extends App { | println("Hello") |} |""".stripMargin ) testInputs.withBuild(baseOptions, buildThreads, None, actionableDiagnostics = true) { (root, _, maybeBuild) => val build = maybeBuild.orThrow val updateDiagnostics = ActionablePreprocessor.generateActionableDiagnostics(build.options).orThrow val actionableDiagnostics = updateDiagnostics.collect { case diagnostic: ActionableDependencyUpdateDiagnostic => diagnostic } val osLib = actionableDiagnostics.find(_.suggestion.startsWith("com.lihaoyi::os-lib")).get val pprintLib = actionableDiagnostics.find(_.suggestion.startsWith("com.lihaoyi::pprint")).get val path = root / "Foo.scala" expect(osLib.positions == Seq(File(Right(path), (0, 14), (0, 39)))) expect(pprintLib.positions == Seq(File(Right(path), (1, 14), (1, 39)))) } } test("using outdated dependencies with --suppress-outdated-dependency-warning") { val dependencyOsLib = "com.lihaoyi::os-lib:0.7.8" val dependencyPprintLib = "com.lihaoyi::pprint:0.6.6" val testInputs = TestInputs( os.rel / "Foo.scala" -> s"""//> using dep $dependencyOsLib |//> using dep $dependencyPprintLib | |object Hello extends App { | println("Hello") |} |""".stripMargin ) val optionsWithSuppress = baseOptions.copy( suppressWarningOptions = SuppressWarningOptions( suppressOutdatedDependencyWarning = Some(true) ) ) testInputs.withBuild(optionsWithSuppress, buildThreads, None, actionableDiagnostics = true) { (_, _, maybeBuild) => val build = maybeBuild.orThrow val updateDiagnostics = ActionablePreprocessor.generateActionableDiagnostics(build.options).orThrow val updateDepsDiagnostics = updateDiagnostics.collect { case diagnostic: ActionableDependencyUpdateDiagnostic => diagnostic } expect(updateDepsDiagnostics.isEmpty) } } test("actionable actions suggest update only to stable version") { val testInputs = TestInputs( os.rel / "Foo.scala" -> s"""//> using dep test-org::test-name-1:1.0.6 | |object Hello extends App { | println("Hello") |} |""".stripMargin ) // create fake repository which contains hardcoded versions [1.0.6, 1.0.7, 1.0.7-M1] of test-name-1 library // scala-cli should skip non-stable version 1.0.7-M1 and suggest update 1.0.7 val repoTmpDir = os.temp.dir(prefix = "scala-cli-tests-actionable-diagnostic-repo") os.write( repoTmpDir / "test-org" / "test-name-1_3" / "maven-metadata.xml", """ | | test-org | test-name-1_3 | | 1.0.7-M1 | 1.0.7-M1 | | 1.0.6 | 1.0.7 | 1.0.7-M1 | | | |""".stripMargin, createFolders = true ) os.write( repoTmpDir / "test-org" / "test-name-1_3" / "1.0.6" / "test-name-1_3-1.0.6.pom", """ | | test-org | test-name-1_3 | 1.0.6 |""".stripMargin, createFolders = true ) val withRepoBuildOptions = baseOptions.copy( classPathOptions = baseOptions.classPathOptions.copy(extraRepositories = Seq(path2url(repoTmpDir))) ) testInputs.withBuild(withRepoBuildOptions, buildThreads, None, actionableDiagnostics = true) { (_, _, maybeBuild) => val build = maybeBuild.orThrow val updateDiagnostics = ActionablePreprocessor.generateActionableDiagnostics(build.options).orThrow val testLibDiagnosticOpt = updateDiagnostics.collectFirst { case diagnostic: ActionableDependencyUpdateDiagnostic => diagnostic } expect(testLibDiagnosticOpt.nonEmpty) val testLibDiagnostic = testLibDiagnosticOpt.get expect(testLibDiagnostic.newVersion == "1.0.7") } } test("actionable actions should not suggest update to previous version") { val testInputs = TestInputs( os.rel / "Foo.scala" -> s"""//> using dep test-org::test-name-1:2.0.0-M1 | |object Hello extends App { | println("Hello") |} |""".stripMargin ) // create fake repository which contains hardcoded versions [1.0.0] of test-name-1 library val repoTmpDir = os.temp.dir(prefix = "scala-cli-tests-actionable-diagnostic-repo") os.write( repoTmpDir / "test-org" / "test-name-1_3" / "maven-metadata.xml", """ | | test-org | test-name-1_3 | | 2.0.0-M | 2.0.0-M1 | | 1.0.0 | 2.0.0-M1 | | | |""".stripMargin, createFolders = true ) os.write( repoTmpDir / "test-org" / "test-name-1_3" / "2.0.0-M1" / "test-name-1_3-2.0.0-M1.pom", """ | | test-org | test-name-1_3 | 2.0.0-M1 |""".stripMargin, createFolders = true ) val withRepoBuildOptions = baseOptions.copy( classPathOptions = baseOptions.classPathOptions.copy(extraRepositories = Seq(path2url(repoTmpDir)) ) ) testInputs.withBuild(withRepoBuildOptions, buildThreads, None, actionableDiagnostics = true) { (_, _, maybeBuild) => val build = maybeBuild.orThrow val updateDiagnostics = ActionablePreprocessor.generateActionableDiagnostics(build.options).orThrow val testLibDiagnosticOpt = updateDiagnostics.collectFirst { case diagnostic: ActionableDependencyUpdateDiagnostic => diagnostic } expect(testLibDiagnosticOpt.isEmpty) } } test("actionable actions should not suggest update if uses version: latest") { val testInputs = TestInputs( os.rel / "Foo.scala" -> s"""//> using toolkit latest | |object Hello extends App { | os.list(os.pwd).foreach(println) |} |""".stripMargin ) testInputs.withBuild(baseOptions, buildThreads, None, actionableDiagnostics = true) { (_, _, maybeBuild) => val build = maybeBuild.orThrow val updateDiagnostics = ActionablePreprocessor.generateActionableDiagnostics(build.options).orThrow val testLibDiagnosticOpt = updateDiagnostics.collectFirst { case diagnostic: ActionableDependencyUpdateDiagnostic => diagnostic } expect(testLibDiagnosticOpt.isEmpty) } } } ================================================ FILE: modules/build/src/test/scala/scala/build/tests/BspServerTests.scala ================================================ package scala.build.tests import com.eed3si9n.expecty.Expecty.expect import java.util.concurrent.TimeUnit import scala.build.Ops.* import scala.build.bsp.{ BspServer, ScalaScriptBuildServer, WrappedSourceItem, WrappedSourcesItem, WrappedSourcesParams, WrappedSourcesResult } import scala.build.options.{BuildOptions, InternalOptions, Scope} import scala.build.{Build, BuildThreads, Directories, GeneratedSource, LocalRepo} import scala.collection.mutable.ArrayBuffer import scala.jdk.CollectionConverters.* class BspServerTests extends TestUtil.ScalaCliBuildSuite { val extraRepoTmpDir: os.Path = os.temp.dir(prefix = "scala-cli-tests-bsp-server-") val directories: Directories = Directories.under(extraRepoTmpDir) val baseOptions = BuildOptions( internal = InternalOptions( localRepository = LocalRepo.localRepo(directories.localRepoDir, TestLogger()) ) ) val buildThreads: BuildThreads = BuildThreads.create() def getScriptBuildServer( generatedSources: Seq[GeneratedSource], workspace: os.Path, scope: Scope = Scope.Main ): ScalaScriptBuildServer = { val bspServer = new BspServer(null, null, null) bspServer.setGeneratedSources(Scope.Main, generatedSources) bspServer.setProjectName(workspace, "test", scope) bspServer } test("correct topWrapper and bottomWrapper for wrapped scripts") { val testInputs = TestInputs( os.rel / "script.sc" -> s"""def msg: String = "Hello" | |println(msg) |""".stripMargin ) testInputs.withBuild(baseOptions, buildThreads, None) { (root, _, maybeBuild) => val build: Build = maybeBuild.orThrow build match { case success: Build.Successful => val generatedSources = success.generatedSources expect(generatedSources.size == 1) val wrappedScript = generatedSources.head val wrappedScriptCode = os.read(wrappedScript.generated) val topWrapper = wrappedScriptCode .linesIterator .take(wrappedScript.wrapperParamsOpt.map(_.topWrapperLineCount).getOrElse(0)) .mkString("", System.lineSeparator(), System.lineSeparator()) val bspServer = getScriptBuildServer(generatedSources, root) val wrappedSourcesResult: WrappedSourcesResult = bspServer .buildTargetWrappedSources(new WrappedSourcesParams(ArrayBuffer.empty.asJava)) .get(10, TimeUnit.SECONDS) val wrappedItems = wrappedSourcesResult.getItems.asScala expect(wrappedItems.size == 1) expect(wrappedItems.head.getSources().asScala.size == 1) expect(wrappedItems.head.getSources().asScala.head.getTopWrapper == topWrapper) case _ => munit.Assertions.fail("Build Failed") } } } } ================================================ FILE: modules/build/src/test/scala/scala/build/tests/BuildOptionsTests.scala ================================================ package scala.build.tests import com.eed3si9n.expecty.Expecty.assert as expect import coursier.Repositories import coursier.cache.FileCache import coursier.maven.MavenRepository import coursier.version.Version import dependency.ScalaParameters import scala.build.Ops.* import scala.build.errors.{ InvalidBinaryScalaVersionError, NoValidScalaVersionFoundError, ScalaVersionError, UnsupportedScalaVersionError } import scala.build.internal.Constants.* import scala.build.internal.Regexes.{scala2NightlyRegex, scala3LtsRegex} import scala.build.options.* import scala.build.tests.util.BloopServer import scala.build.{Build, BuildThreads, Directories, LocalRepo, Positioned, RepositoryUtils} import scala.concurrent.duration.DurationInt class BuildOptionsTests extends TestUtil.ScalaCliBuildSuite { override def munitFlakyOK: Boolean = TestUtil.isCI val extraRepoTmpDir: os.Path = os.temp.dir(prefix = "scala-cli-tests-extra-repo-") val directories: Directories = Directories.under(extraRepoTmpDir) val buildThreads: BuildThreads = BuildThreads.create() val baseOptions = BuildOptions( internal = InternalOptions( localRepository = LocalRepo.localRepo(directories.localRepoDir, TestLogger()), keepDiagnostics = true ) ) def bloopConfigOpt = Some(BloopServer.bloopConfig) override def afterAll(): Unit = { buildThreads.shutdown() } test("Empty BuildOptions is actually empty") { val empty = BuildOptions() val zero = BuildOptions.monoid.zero expect( empty == zero, "Unexpected Option / Seq / Set / Boolean with a non-empty / non-false default value" ) } test("-S 3.nightly option works") { val options = BuildOptions( scalaOptions = ScalaOptions( scalaVersion = Some(MaybeScalaVersion("3.nightly")) ) ) val scalaParams = options.scalaParams.orThrow.getOrElse(sys.error("should not happen")) assert( scalaParams.scalaVersion.startsWith("3") && scalaParams.scalaVersion.endsWith("-NIGHTLY"), "-S 3.nightly argument does not lead to scala3 nightly build option" ) } test("-S 3.1.nightly option works") { val options = BuildOptions( scalaOptions = ScalaOptions( scalaVersion = Some(MaybeScalaVersion("3.1.nightly")) ) ) val scalaParams = options.scalaParams.orThrow.getOrElse(sys.error("should not happen")) expect( scalaParams.scalaVersion.startsWith("3.1.") && scalaParams.scalaVersion.endsWith("-NIGHTLY"), "-S 3.1.nightly argument does not lead to scala 3.1. nightly build option" ) } test(s"Scala 3.${Int.MaxValue} shows Invalid Binary Scala Version Error") { val options = BuildOptions( scalaOptions = ScalaOptions( scalaVersion = Some(MaybeScalaVersion(s"3.${Int.MaxValue}")) ) ) assert( options.projectParams.swap.exists { case _: InvalidBinaryScalaVersionError => true; case _ => false }, s"specifying the 3.${Int.MaxValue} scala version does not lead to the Invalid Binary Scala Version Error" ) } test(s"Scala 2.lts shows Scala Version Error") { val options = BuildOptions( scalaOptions = ScalaOptions( scalaVersion = Some(MaybeScalaVersion(s"3.${Int.MaxValue}")) ) ) assert( options.projectParams.swap.exists { case _: ScalaVersionError => true; case _ => false }, s"specifying 2.lts scala version does not lead to Scala Version Error" ) } test("Scala 2.11.2 shows Unupported Scala Version Error") { val options = BuildOptions( scalaOptions = ScalaOptions( scalaVersion = Some(MaybeScalaVersion("2.11.2")) ) ) assert( options.projectParams.swap.exists { case _: UnsupportedScalaVersionError => true; case _ => false }, "specifying the 2.11.2 scala version does not lead to the Unsupported Scala Version Error" ) } test("Scala 2.11 shows Unupported Scala Version Error") { val options = BuildOptions( scalaOptions = ScalaOptions( scalaVersion = Some(MaybeScalaVersion("2.11")) ) ) assert( options.projectParams.swap.exists { case _: UnsupportedScalaVersionError => true; case _ => false }, "specifying the 2.11 scala version does not lead to the Unsupported Scala Version Error" ) } test(s"Scala 3.${Int.MaxValue}.3 shows Invalid Binary Scala Version Error") { val options = BuildOptions( scalaOptions = ScalaOptions( scalaVersion = Some(MaybeScalaVersion(s"3.${Int.MaxValue}.3")) ) ) assert( options.projectParams.swap.exists { case _: InvalidBinaryScalaVersionError => true; case _ => false }, "specifying the 3.2147483647.3 scala version does not lead to the Invalid Binary Scala Version Error" ) } test("Scala 3.1.3-RC1-bin-20220213-fd97eee-NIGHTLY shows No Valid Scala Version Error") { val options = BuildOptions( scalaOptions = ScalaOptions( scalaVersion = Some(MaybeScalaVersion("3.1.3-RC1-bin-20220213-fd97eee-NIGHTLY")) ) ) assert( options.projectParams.swap.exists { case _: NoValidScalaVersionFoundError => true; case _ => false }, "specifying the wrong full scala 3 nightly version does not lead to the No Valid Scala Version Found Error" ) } test("Scala 3.1.2-RC1 works") { val options = BuildOptions( scalaOptions = ScalaOptions( scalaVersion = Some(MaybeScalaVersion("3.1.2-RC1")) ) ) val scalaParams = options.scalaParams.orThrow.getOrElse(sys.error("should not happen")) assert( scalaParams.scalaVersion == "3.1.2-RC1", "-S 3.1.2-RC1 argument does not lead to 3.1.2-RC1 build option" ) } test("Scala 2.12.9-bin-1111111 shows No Valid Scala Version Error".flaky) { val options = BuildOptions( scalaOptions = ScalaOptions( scalaVersion = Some(MaybeScalaVersion("2.12.9-bin-1111111")) ) ) assert( options.projectParams.swap.exists { case _: NoValidScalaVersionFoundError => true; case _ => false }, "specifying the wrong full scala 2 nightly version does not lead to the No Valid Scala Version Found Error" ) } test(s"Scala 2.${Int.MaxValue} shows Invalid Binary Scala Version Error") { val options = BuildOptions( scalaOptions = ScalaOptions( scalaVersion = Some(MaybeScalaVersion(s"2.${Int.MaxValue}")) ) ) assert( options.projectParams.swap.exists { case _: InvalidBinaryScalaVersionError => true; case _ => false }, s"specifying 2.${Int.MaxValue} as Scala version does not lead to Invalid Binary Scala Version Error" ) } test("-S 2.nightly option works") { val options = BuildOptions( scalaOptions = ScalaOptions( scalaVersion = Some(MaybeScalaVersion("2.nightly")) ) ) val scalaParams = options.scalaParams.orThrow.getOrElse(sys.error("should not happen")) assert( scala2NightlyRegex.unapplySeq(scalaParams.scalaVersion).isDefined, "-S 2.nightly argument does not lead to scala2 nightly build option" ) } test("-S 2.13.nightly option works") { val options = BuildOptions( scalaOptions = ScalaOptions( scalaVersion = Some(MaybeScalaVersion("2.13.nightly")) ) ) val scalaParams = options.scalaParams.orThrow.getOrElse(sys.error("should not happen")) assert( scala2NightlyRegex.unapplySeq(scalaParams.scalaVersion).isDefined, "-S 2.13.nightly argument does not lead to scala2 nightly build option" ) } test("-S 3.lts option works") { val options = BuildOptions( scalaOptions = ScalaOptions( scalaVersion = Some(MaybeScalaVersion("3.lts")) ) ) val scalaParams = options.scalaParams.orThrow.getOrElse(sys.error("should not happen")) assert( scala3LtsRegex.unapplySeq(scalaParams.scalaVersion).isDefined, "-S 3.lts argument does not lead to scala3 LTS" ) } test("-S 2.12.nightly option works") { val options = BuildOptions( scalaOptions = ScalaOptions( scalaVersion = Some(MaybeScalaVersion("2.12.nightly")) ) ) val scalaParams = options.scalaParams.orThrow.getOrElse(sys.error("should not happen")) assert( scala2NightlyRegex.unapplySeq(scalaParams.scalaVersion).isDefined, "-S 2.12.nightly argument does not lead to scala2 nightly build option" ) } test("-S 2.13.9-bin-4505094 option works without repo specification".flaky) { val options = BuildOptions( scalaOptions = ScalaOptions( scalaVersion = Some(MaybeScalaVersion("2.13.9-bin-4505094")) ) ) val scalaParams = options.scalaParams.orThrow.getOrElse(sys.error("should not happen")) assert( scalaParams.scalaVersion == "2.13.9-bin-4505094", "-S 2.13.9-bin-4505094 argument does not lead to 2.13.9-bin-4505094 scala version in build option" ) } test("Empty BuildRequirements is actually empty") { val empty = BuildRequirements() val zero = BuildRequirements.monoid.zero expect( empty == zero, "Unexpected Option / Seq / Set / Boolean with a non-empty / non-false default value" ) } val expectedScalaVersions: Seq[(Option[String], String)] = Seq( None -> defaultScalaVersion, Some("2.13.2") -> "2.13.2", Some("3.0.1") -> "3.0.1", Some("3.0") -> "3.0.2" ) for ((prefix, expectedScalaVersion) <- expectedScalaVersions) test( s"use scala $expectedScalaVersion for prefix scala version: ${prefix.getOrElse("empty")}" ) { val options = BuildOptions( scalaOptions = ScalaOptions( scalaVersion = prefix.map(MaybeScalaVersion(_)) ), internal = InternalOptions( cache = Some(FileCache().withTtl(0.seconds)) ) ) val scalaParams = options.scalaParams.orThrow.getOrElse(sys.error("should not happen")) val expectedScalaParams = ScalaParameters(expectedScalaVersion) expect(scalaParams == expectedScalaParams) } { val cache = FileCache().withTtl(0.seconds) val repositories = BuildOptions( internal = InternalOptions(cache = Some(cache)), classPathOptions = ClassPathOptions( extraRepositories = Seq( coursier.Repositories.scalaIntegration.root, RepositoryUtils.scala3NightlyRepository.root ) ) ).finalRepositories.orThrow val allScalaVersions = ScalaVersionUtil.allMatchingVersions(None, cache, repositories) for { (prefix, defaultMatchingVersion, predefinedDefaultScalaVersion) <- { extension (nightlies: Seq[String]) private def latestNightly: Option[String] = if nightlies.nonEmpty then Some(nightlies.maxBy(Version(_))) else None val scala2Nightlies = allScalaVersions.filter(ScalaVersionUtil.isScala2Nightly) val scala212Nightlies = scala2Nightlies.filter(_.startsWith("2.12")) val scala213Nightlies = scala2Nightlies.filter(_.startsWith("2.13")) val scala3Nightlies = allScalaVersions.filter(ScalaVersionUtil.isScala3Nightly) val scala3NextNightlies = scala3Nightlies.filter(_.startsWith(scala3NextPrefix)) Seq( ("2.12", defaultScala212Version, None), ("2.12", defaultScala212Version, scala212Nightlies.latestNightly), ("2.13", defaultScala213Version, None), ("2.13", defaultScala213Version, scala213Nightlies.latestNightly), ("3", defaultScalaVersion, None), (scala3NextPrefix, defaultScalaVersion, None), (scala3NextPrefix, defaultScalaVersion, scala3NextNightlies.latestNightly) ).distinct } options = BuildOptions( scalaOptions = ScalaOptions( scalaVersion = Some(prefix).map(MaybeScalaVersion(_)), defaultScalaVersion = predefinedDefaultScalaVersion ), internal = InternalOptions( cache = Some(cache) ), classPathOptions = ClassPathOptions( extraRepositories = Seq(coursier.Repositories.scalaIntegration.root) ) ) latestMatchingVersion = allScalaVersions .filter(ScalaVersionUtil.isStable) .filter(_.startsWith(prefix)) .maxBy(Version(_)) expectedVersion = predefinedDefaultScalaVersion.getOrElse(defaultMatchingVersion) expectedVersionDescription = if expectedVersion == defaultMatchingVersion then "default" else "overridden default" launcherDefaultVersionDescription = if expectedVersion == defaultMatchingVersion then "" else s"or the launcher default ($defaultMatchingVersion)" testDescription = s"-S $prefix should choose the $expectedVersionDescription version ($expectedVersion), not necessarily the latest stable ($latestMatchingVersion) $launcherDefaultVersionDescription" } test(testDescription) { val scalaParams = options.scalaParams.orThrow.getOrElse(sys.error("should not happen")) val expectedScalaParams = ScalaParameters(expectedVersion) expect(scalaParams == expectedScalaParams, s"expected $expectedScalaParams, got $scalaParams") } } test("User scalac options shadow internal ones") { val defaultOptions = BuildOptions( internal = InternalOptions( localRepository = LocalRepo.localRepo(directories.localRepoDir, TestLogger()) ) ) val newSourceRoot = os.pwd / "out" / "foo" val extraScalacOpt = Seq("-sourceroot", newSourceRoot.toString) val options = defaultOptions.copy( scalaOptions = defaultOptions.scalaOptions.copy( scalaVersion = Some(MaybeScalaVersion("3.1.1")), scalacOptions = ShadowingSeq.from( extraScalacOpt .map(ScalacOpt(_)) .map(Positioned.none) ) ) ) val dummyInputs = TestInputs( os.rel / "Foo.scala" -> """object Foo |""".stripMargin ) dummyInputs.withLoadedBuild(options, buildThreads, None) { (_, _, build) => val build0 = build match { case s: Build.Successful => s case _ => sys.error(s"Unexpected failed or cancelled build $build") } val rawOptions = build0.project.scalaCompiler.toSeq.flatMap(_.scalacOptions) val seq = ShadowingSeq.from(rawOptions.map(ScalacOpt(_))) expect(seq.toSeq.length == rawOptions.length) // no option needs to be shadowed pprint.err.log(rawOptions) expect(rawOptions.containsSlice(extraScalacOpt)) } } test("parse snapshots repository") { val inputs = TestInputs( os.rel / "Foo.scala" -> """//> using repository snapshots |//> using repository central |object Foo extends App { | println("Hello") |} |""".stripMargin ) inputs.withBuild(BuildOptions(), buildThreads, bloopConfigOpt, buildTests = false) { (_, _, maybeBuild) => expect(maybeBuild.exists(_.success)) val build = maybeBuild .toOption .flatMap(_.successfulOpt) .getOrElse(sys.error("cannot happen")) val repositories = build.options.finalRepositories.orThrow expect(repositories.length == 3) expect(repositories.contains(Repositories.central)) expect(repositories.contains(RepositoryUtils.snapshotsRepository)) expect(repositories.contains(RepositoryUtils.scala3NightlyRepository)) } } test("resolve semanticDB for older scala version") { val buildOptions = BuildOptions() val scalaVersion = "2.13.3" val semanticDbVersion = buildOptions.findSemanticDbVersion(scalaVersion, TestLogger()) expect(semanticDbVersion == "4.8.4") } test("skip setting release option when -release or -java-output-version is set by user") { val javaOutputVersionOpt = s"-java-output-version:${scala.build.internal.Constants.scala38MinJavaVersion}" val inputs = TestInputs( os.rel / "Hello.scala" -> s"""//> using option $javaOutputVersionOpt |""".stripMargin ) inputs.withBuild(BuildOptions(), buildThreads, bloopConfigOpt, buildTests = false) { (_, _, maybeBuild) => expect(maybeBuild.exists(_.success)) val build = maybeBuild .toOption .flatMap(_.successfulOpt) .getOrElse(sys.error("cannot happen")) val scalacOpts = build.project.scalaCompiler.toSeq.flatMap(_.scalacOptions) expect(scalacOpts.contains(javaOutputVersionOpt)) expect(!scalacOpts.exists(_.startsWith("-release"))) } } } ================================================ FILE: modules/build/src/test/scala/scala/build/tests/BuildProjectTests.scala ================================================ package scala.build.tests import bloop.rifle.BloopRifleLogger import com.eed3si9n.expecty.Expecty.expect import coursier.cache.CacheLogger import org.scalajs.logging.{Logger as ScalaJsLogger, NullLogger} import java.io.PrintStream import scala.build.Ops.* import scala.build.errors.{BuildException, Diagnostic} import scala.build.input.Inputs import scala.build.internals.FeatureType import scala.build.options.{BuildOptions, InternalOptions, Scope} import scala.build.{Build, LocalRepo, Logger, Sources} class BuildProjectTests extends TestUtil.ScalaCliBuildSuite { class LoggerMock extends Logger { var diagnostics: List[Diagnostic] = Nil override def error(message: String): Unit = sys.error(message) override def message(message: => String): Unit = System.err.println(message) override def log(s: => String): Unit = System.err.println(s) override def log(s: => String, debug: => String): Unit = System.err.println(s) override def debug(s: => String): Unit = System.err.println(s) override def log(diagnostics: Seq[Diagnostic]): Unit = { this.diagnostics = this.diagnostics ++ diagnostics } override def log(ex: BuildException): Unit = { ex.printStackTrace() System.err.println(ex.message) } override def debug(ex: BuildException): Unit = { ex.printStackTrace() System.err.println(ex.message) } override def exit(ex: BuildException): Nothing = { ex.printStackTrace() System.err.println(ex.message) sys.exit(1) } override def coursierLogger(message: String): CacheLogger = CacheLogger.nop override def bloopRifleLogger: BloopRifleLogger = BloopRifleLogger.nop override def scalaJsLogger: ScalaJsLogger = NullLogger override def scalaNativeTestLogger: scala.scalanative.build.Logger = scala.scalanative.build.Logger.nullLogger override def scalaNativeCliInternalLoggerOptions: List[String] = List() override def compilerOutputStream: PrintStream = System.out override def verbosity: Int = 0 override def experimentalWarning(featureName: String, featureType: FeatureType): Unit = System.err.println(s"experimental: $featureName") override def flushExperimentalWarnings: Unit = () } test("workspace for bsp") { val options = BuildOptions( internal = InternalOptions(localRepository = LocalRepo.localRepo(scala.build.Directories.default().localRepoDir, TestLogger()) ) ) val inputs = Inputs.empty("project") val sources = Sources(Nil, Nil, None, Nil, options) val logger = new LoggerMock() val artifacts = options.artifacts(logger, Scope.Test).orThrow val project = Build.buildProject(inputs, sources, Nil, options, Scope.Main, logger, artifacts).orThrow expect(project.workspace == inputs.workspace) } } ================================================ FILE: modules/build/src/test/scala/scala/build/tests/BuildTests.scala ================================================ package scala.build.tests import bloop.config.Config.LinkerMode import bloop.rifle.BloopRifleConfig import ch.epfl.scala.bsp4j import com.eed3si9n.expecty.Expecty.expect import com.google.gson.Gson import dependency.parser.DependencyParser import java.io.IOException import scala.build.Ops.* import scala.build.errors.{ DependencyFormatError, InvalidBinaryScalaVersionError, ScalaNativeCompatibilityError } import scala.build.options.* import scala.build.tastylib.TastyData import scala.build.tests.TestUtil.* import scala.build.tests.util.BloopServer import scala.build.{Build, BuildThreads, Directories, LocalRepo, Positioned} import scala.jdk.CollectionConverters.* import scala.meta.internal.semanticdb.TextDocuments import scala.util.Properties abstract class BuildTests(server: Boolean) extends TestUtil.ScalaCliBuildSuite { private def hasDiagnostics = server val buildThreads: BuildThreads = BuildThreads.create() def bloopConfigOpt: Option[BloopRifleConfig] = if server then Some(BloopServer.bloopConfig) else None val extraRepoTmpDir: os.Path = os.temp.dir(prefix = "scala-cli-tests-extra-repo-") val directories: Directories = Directories.under(extraRepoTmpDir) override def afterAll(): Unit = { TestInputs.tryRemoveAll(extraRepoTmpDir) buildThreads.shutdown() } val baseOptions = BuildOptions( internal = InternalOptions( localRepository = LocalRepo.localRepo(directories.localRepoDir, TestLogger()), keepDiagnostics = true ) ) def sv2: String = Constants.defaultScala213Version val defaultOptions: BuildOptions = baseOptions.copy( scalaOptions = baseOptions.scalaOptions.copy( scalaVersion = Some(MaybeScalaVersion(sv2)), scalaBinaryVersion = None ), scriptOptions = ScriptOptions(Some(true)) ) def sv3: String = Constants.defaultScalaVersion val defaultScala3Options: BuildOptions = defaultOptions.copy( scalaOptions = defaultOptions.scalaOptions.copy( scalaVersion = Some(MaybeScalaVersion(sv3)), scalaBinaryVersion = None ), scriptOptions = ScriptOptions(None) ) def simple(checkResults: Boolean = true): Unit = { val testInputs = TestInputs( os.rel / "simple.sc" -> """val n = 2 |println(s"n=$n") |""".stripMargin ) testInputs.withBuild(defaultOptions, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => if (checkResults) maybeBuild.orThrow.assertGeneratedEquals( "simple.class", "simple$.class", "simple$delayedInit$body.class" ) } } try simple(checkResults = false) catch { case _: IOException => // ignored } test("simple") { simple() } test("scala 3") { val testInputs = TestInputs( os.rel / "simple.sc" -> """val n = 2 |println(s"n=$n") |""".stripMargin ) testInputs.withBuild(defaultScala3Options, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => maybeBuild.orThrow.assertGeneratedEquals( "simple$_.class", "simple_sc.class", "simple_sc.tasty", "simple$_.tasty", "simple_sc$.class", "simple$package$.class", "simple$package.class", "simple$package.tasty" ) maybeBuild.orThrow.assertNoDiagnostics() } } // Test if we do not print any warnings test("scala 3 class in .sc file") { val testInputs = TestInputs( os.rel / "other.sc" -> "class A" ) testInputs.withBuild(defaultScala3Options, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => val build = maybeBuild.orThrow build.assertGeneratedEquals( "other$_$A.class", "other$_.tasty", "other$_.class", "other_sc$.class", "other_sc.class", "other_sc.tasty", "other$package$.class", "other$package.class", "other$package.tasty" ) maybeBuild.orThrow.assertNoDiagnostics() } } test("semantic DB") { import scala.meta.internal.semanticdb.* val scriptContents = """val n = 2 |println(s"n=$n") |""".stripMargin val expectedSymbolOccurences = Seq( SymbolOccurrence( Some(Range(0, 4, 0, 5)), "_empty_/simple.n.", SymbolOccurrence.Role.DEFINITION ), SymbolOccurrence( Some(Range(1, 8, 1, 9)), "scala/StringContext#s().", SymbolOccurrence.Role.REFERENCE ), SymbolOccurrence( Some(Range(1, 0, 1, 7)), "scala/Predef.println(+1).", SymbolOccurrence.Role.REFERENCE ), SymbolOccurrence( Some(Range(1, 13, 1, 14)), "_empty_/simple.n.", SymbolOccurrence.Role.REFERENCE ) ) val testInputs = TestInputs(os.rel / "simple.sc" -> scriptContents) val buildOptions = defaultOptions.copy( scalaOptions = defaultOptions.scalaOptions.copy( semanticDbOptions = defaultOptions.scalaOptions.semanticDbOptions.copy( generateSemanticDbs = Some(true) ) ) ) testInputs.withBuild(buildOptions, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => val build = maybeBuild.orThrow build.assertGeneratedEquals( "simple$delayedInit$body.class", "simple$.class", "simple.class", "META-INF/semanticdb/simple.sc.semanticdb" ) maybeBuild.orThrow.assertNoDiagnostics() val outputDir = build.outputOpt.getOrElse(sys.error("no build output???")) val semDb = os.read.bytes(outputDir / "META-INF" / "semanticdb" / "simple.sc.semanticdb") val doc = TextDocuments.parseFrom(semDb) val uris = doc.documents.map(_.uri) expect(uris == Seq("simple.sc")) val occurences = doc.documents.flatMap(_.occurrences) expect(occurences.forall(_.range.isDefined)) val sortedOccurences = doc.documents.flatMap(_.occurrences) .sortBy(s => s.range.map(r => (r.startLine, r.startCharacter)).getOrElse((Int.MaxValue, Int.MaxValue)) ) val sortedExpectedOccurences = expectedSymbolOccurences .sortBy(s => s.range.map(r => (r.startLine, r.startCharacter)).getOrElse((Int.MaxValue, Int.MaxValue)) ) munit.Assertions.assert( sortedOccurences == sortedExpectedOccurences, clue = doc.documents.flatMap(_.occurrences) ) } } test("TASTy") { val testInputs = TestInputs( os.rel / "simple.sc" -> """val n = 2 |println(s"n=$n") |""".stripMargin ) val buildOptions = defaultScala3Options.copy( scalaOptions = defaultScala3Options.scalaOptions.copy( semanticDbOptions = defaultScala3Options.scalaOptions.semanticDbOptions.copy( generateSemanticDbs = Some(true) ) ) ) testInputs.withBuild(buildOptions, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => val build = maybeBuild.orThrow build.assertGeneratedEquals( "simple$_.class", "simple_sc.class", "simple_sc.tasty", "simple$_.tasty", "simple_sc$.class", "simple$package$.class", "simple$package.class", "simple$package.tasty", "META-INF/semanticdb/simple.sc.semanticdb" ) maybeBuild.orThrow.assertNoDiagnostics() val outputDir = build.outputOpt.getOrElse(sys.error("no build output???")) val tastyData = TastyData.read(os.read.bytes(outputDir / "simple$_.tasty")).orThrow val names = tastyData.names.simpleNames expect(names.contains("simple.sc")) } } test("simple JS") { val testInputs = TestInputs( os.rel / "simple.sc" -> """val n = 2 |println(s"n=$n") |""".stripMargin ) testInputs.withBuild(defaultOptions.enableJs, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => maybeBuild.orThrow.assertGeneratedEquals( "simple$.class", "simple$.sjsir", "simple$delayedInit$body.class", "simple$delayedInit$body.sjsir", "simple.class", "simple.sjsir" ) maybeBuild.orThrow.assertNoDiagnostics() } } def simpleNativeTest(): Unit = { val testInputs = TestInputs( os.rel / "simple.sc" -> """val n = 2 |println(s"n=$n") |""".stripMargin ) testInputs.withBuild(defaultOptions.enableNative, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => maybeBuild.orThrow.assertGeneratedEquals( "simple$.class", "simple$.nir", "simple$delayedInit$body.class", "simple$delayedInit$body.nir", "simple.class", "simple.nir" ) maybeBuild.orThrow.assertNoDiagnostics() } } if (!Properties.isWin) test("simple native") { simpleNativeTest() } test("dependencies - using") { val testInputs = TestInputs( os.rel / "simple.sc" -> """//> using dep com.lihaoyi::geny:0.6.5 |import geny.Generator |val g = Generator("Hel", "lo") |println(g.mkString) |""".stripMargin ) testInputs.withBuild(defaultOptions, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => maybeBuild.orThrow.assertGeneratedEquals( "simple.class", "simple$.class", "simple$delayedInit$body.class" ) maybeBuild.orThrow.assertNoDiagnostics() } } test("several dependencies - using") { val testInputs = TestInputs( os.rel / "simple.sc" -> """//> using dep com.lihaoyi::geny:0.6.5 |//> using dep com.lihaoyi::pprint:0.6.6 |import geny.Generator |val g = Generator("Hel", "lo") |pprint.log(g) |""".stripMargin, os.rel / "simple2.sc" -> """//> using dep com.lihaoyi::geny:0.6.5 com.lihaoyi::pprint:0.6.6 |import geny.Generator |val g = Generator("Hel", "lo") |pprint.log(g) |""".stripMargin ) testInputs.withBuild(defaultOptions, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => maybeBuild.orThrow.assertGeneratedEquals( "simple$.class", "simple$delayedInit$body.class", "simple.class", "simple2$.class", "simple2$delayedInit$body.class", "simple2.class" ) maybeBuild.orThrow.assertNoDiagnostics() } } if (hasDiagnostics) test("diagnostics") { diagnosticsTest() } def diagnosticsTest(): Unit = { val testInputs = TestInputs( os.rel / "simple.sc" -> """val n = 2 |println(s"n=$n") |zz |""".stripMargin ) testInputs.withBuild(defaultOptions, buildThreads, bloopConfigOpt) { (root, _, maybeBuild) => val expectedDiag = { val start = new bsp4j.Position(2, 0) val end = new bsp4j.Position(2, 2) val range = new bsp4j.Range(start, end) val d = new bsp4j.Diagnostic(range, "not found: value zz") d.setSource("bloop") d.setSeverity(bsp4j.DiagnosticSeverity.ERROR) val bScalaDiagnostic = new bsp4j.ScalaDiagnostic bScalaDiagnostic.setActions(List().asJava) d.setData(new Gson().toJsonTree(bScalaDiagnostic)) d } val diagnostics = maybeBuild.orThrow.diagnostics val expected = Some(Seq(Right(root / "simple.sc") -> expectedDiag)) expect(diagnostics == expected) } } if (hasDiagnostics) test("diagnostics Scala 3") { scala3DiagnosticsTest() } def scala3DiagnosticsTest(): Unit = { val testInputs = TestInputs( os.rel / "simple.sc" -> """val n = 2 |println(s"n=$n") |zz |""".stripMargin ) testInputs.withBuild(defaultScala3Options, buildThreads, bloopConfigOpt) { (root, _, maybeBuild) => val expectedDiag = { val start = new bsp4j.Position(2, 0) val end = new bsp4j.Position(2, 2) val range = new bsp4j.Range(start, end) val d = new bsp4j.Diagnostic(range, "Not found: zz") d.setSource("bloop") d.setCode("6") d.setSeverity(bsp4j.DiagnosticSeverity.ERROR) val bScalaDiagnostic = new bsp4j.ScalaDiagnostic bScalaDiagnostic.setActions(List().asJava) d.setData(new Gson().toJsonTree(bScalaDiagnostic)) d } val diagnostics = maybeBuild.orThrow.diagnostics val expected = Some(Seq(Right(root / "simple.sc") -> expectedDiag)) expect(diagnostics == expected) } } test("ignore files if wrong Scala version requirement") { val testInputs = TestInputs( os.rel / "Simple.scala" -> """object Simple { | def main(args: Array[String]): Unit = | println("Hello") |} |""".stripMargin, os.rel / "Ignored.scala" -> """//> using target.scala.== 2.12 |object Ignored { | def foo = 2 |} |""".stripMargin ) testInputs.withBuild(defaultOptions, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => maybeBuild.orThrow.assertGeneratedEquals( "Simple.class", "Simple$.class" ) } } test("ignore files if wrong Scala target requirement") { val testInputs = TestInputs( os.rel / "Simple.scala" -> """object Simple { | def main(args: Array[String]): Unit = | println("Hello") |} |""".stripMargin, os.rel / "Ignored.scala" -> """//> using target.platform scala.js |object Ignored { | def foo = 2 |} |""".stripMargin ) testInputs.withBuild(defaultOptions, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => maybeBuild.orThrow.assertGeneratedEquals( "Simple.class", "Simple$.class" ) } } test("ignore files if wrong Scala target requirement - JS") { val testInputs = TestInputs( os.rel / "Simple.scala" -> """object Simple { | def main(args: Array[String]): Unit = | println("Hello") |} |""".stripMargin, os.rel / "Ignored.scala" -> """//> using target.platform jvm |object Ignored { | def foo = 2 |} |""".stripMargin, os.rel / "IgnoredToo.scala" -> """//> using target.platform native |object IgnoredToo { | def foo = 2 |} |""".stripMargin ) val options = defaultOptions.enableJs testInputs.withBuild(options, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => maybeBuild.orThrow.assertGeneratedEquals( "Simple.class", "Simple$.class", "Simple.sjsir", "Simple$.sjsir" ) } } test("Pass files with only commented directives as is to scalac") { val testInputs = TestInputs( os.rel / "Simple.scala" -> """//> using dep com.lihaoyi::pprint:0.6.6 |object Simple { | def main(args: Array[String]): Unit = | pprint.log("Hello " + "from tests") |} |""".stripMargin ) testInputs.withBuild(defaultOptions, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => val sources = maybeBuild.toOption.get.successfulOpt.get.sources expect(sources.inMemory.isEmpty) expect(sources.paths.lengthCompare(1) == 0) } } test("Compiler plugins from using directives") { val inputs = TestInputs( os.rel / "p.sc" -> """//> using scala 2.13 |//> using plugins com.olegpy::better-monadic-for:0.3.1 | |def getCounts: Either[String, (Int, Int)] = ??? | |for { | (x, y) <- getCounts |} yield x + y |""".stripMargin ) inputs.withBuild(defaultOptions, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => assert(clue(maybeBuild.orThrow.diagnostics).toSeq.flatten.isEmpty) } } test("Scala Native working with Scala 3.1") { val testInputs = TestInputs( os.rel / "Simple.scala" -> """//> using platform scala-native |//> using nativeVersion 0.4.3-RC2 |//> using scala 3.1.0 |def foo(): String = "foo" |""".stripMargin ) val buildOptions = defaultOptions.copy( scalaOptions = defaultOptions.scalaOptions.copy( scalaVersion = None ) ) testInputs.withBuild(buildOptions, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => assert(maybeBuild.isRight) } } test("Scala Native not working with Scala 3.0") { val testInputs = TestInputs( os.rel / "Simple.scala" -> """//> using platform scala-native |//> using nativeVersion 0.4.3-RC2 |//> using scala 3.0.2 |def foo(): String = "foo" |""".stripMargin ) val buildOptions = defaultOptions.copy( scalaOptions = defaultOptions.scalaOptions.copy( scalaVersion = None ) ) testInputs.withBuild(buildOptions, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => assert(maybeBuild.isLeft) assert( maybeBuild.swap.toOption.exists { case _: ScalaNativeCompatibilityError => true case _ => false } ) } } test(s"Scala 3.${Int.MaxValue}.3 makes the build fail with InvalidBinaryScalaVersionError") { val testInputs = TestInputs( os.rel / "Simple.scala" -> s""" // using scala "3.${Int.MaxValue}.3" |object Hello { | def main(args: Array[String]): Unit = | println("Hello") |} | |""".stripMargin ) val buildOptions = baseOptions.copy( scalaOptions = baseOptions.scalaOptions.copy( scalaVersion = Some(MaybeScalaVersion(s"3.${Int.MaxValue}.3")), scalaBinaryVersion = None ) ) testInputs.withBuild(buildOptions, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => assert( maybeBuild.swap.exists { case _: InvalidBinaryScalaVersionError => true; case _ => false }, s"specifying Scala 3.${Int.MaxValue}.3 as version does not lead to InvalidBinaryScalaVersionError" ) } } test("cli dependency options shadowing using directives") { val usingDependency = "org.scalameta::munit::1.0.0-M1" val cliDependency = "org.scalameta::munit::1.1.1" val inputs = TestInputs( os.rel / "foo.scala" -> s"""//> using dep $usingDependency |def foo = "bar" |""".stripMargin ) val parsedCliDependency = DependencyParser.parse(cliDependency).getOrElse( throw new DependencyFormatError(cliDependency, "", Nil) ) // Emulates options derived from cli val buildOptions = defaultOptions.copy( classPathOptions = defaultOptions.classPathOptions.copy( extraDependencies = ShadowingSeq.from(Seq(Positioned.none(parsedCliDependency))) ) ) inputs.withBuild(buildOptions, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => assert(maybeBuild.isRight) val build = maybeBuild.toOption.get val artifacts = build.options.classPathOptions.extraDependencies.toSeq assert(artifacts.exists(_.value.toString() == cliDependency)) } } test("cli scalac options shadowing using directives") { val cliScalacOptions = Seq("-Xmaxwarns", "4", "-g:source", "-language:no2AutoTupling", "-language", "no2AutoTupling") val usingDirectiveScalacOptions = Seq( "-nobootcp", "-Xmaxwarns", "5", "-g:none", "-language:no2AutoTupling", "-language:strictEquality" ) val expectedOptions = Seq( "-Xmaxwarns", "4", "-g:source", "-language:no2AutoTupling", "-nobootcp", "-language:strictEquality" ) val inputs = TestInputs( os.rel / "foo.scala" -> s"""//> using options ${usingDirectiveScalacOptions.mkString(" ")} |def foo = "bar" |""".stripMargin ) // Emulates options derived from cli val buildOptions = defaultOptions.copy( scalaOptions = defaultOptions.scalaOptions.copy( scalacOptions = ShadowingSeq.from( cliScalacOptions.map(ScalacOpt(_)).map(Positioned.commandLine) ) ) ) inputs.withBuild(buildOptions, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => assert(maybeBuild.isRight) val build = maybeBuild.toOption.get val scalacOptions = build.options.scalaOptions.scalacOptions.toSeq.map(_.value.value) assertEquals(scalacOptions, expectedOptions) } } test("cli java options shadowing using directives") { val cliJavaOptions = Seq("-proc:only", "-JflagB", "-Xmx2G") val usingDirectiveJavaOptions = Seq("-proc:none", "-parameters", "-JflagA", "-Xmx4G") val expectedJavaOptions = Seq("-proc:only", "-JflagB", "-Xmx2G", "-parameters", "-JflagA") val inputs = TestInputs( os.rel / "foo.scala" -> s"""//> using javaOpt ${usingDirectiveJavaOptions.mkString(" ")} |def foo = "bar" |""".stripMargin ) // Emulates options derived from cli val buildOptions = defaultOptions.copy( javaOptions = defaultOptions.javaOptions.copy( javaOpts = ShadowingSeq.from( cliJavaOptions.map(JavaOpt(_)).map(Positioned.commandLine) ) ) ) inputs.withBuild(buildOptions, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => val build = maybeBuild.orThrow val javaOptions = build.options.javaOptions.javaOpts.toSeq.map(_.value.value) assert(javaOptions == expectedJavaOptions) } } test("repeated Java options") { val inputs = TestInputs( os.rel / "foo.sc" -> """//> using javaOpt --add-opens foo/bar |//> using javaOpt --add-opens other/thing |//> using javaOpt --add-exports foo/bar |//> using javaOpt --add-exports other/thing |//> using javaOpt --add-modules foo/bar |//> using javaOpt --add-modules other/thing |//> using javaOpt --add-reads foo/bar |//> using javaOpt --add-reads other/thing |//> using javaOpt --patch-module foo/bar |//> using javaOpt --patch-module other/thing | |def foo = "bar" |""".stripMargin ) inputs.withBuild(defaultOptions, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => val expectedOptions = // format: off Seq( "--add-opens", "foo/bar", "--add-opens", "other/thing", "--add-exports", "foo/bar", "--add-exports", "other/thing", "--add-modules", "foo/bar", "--add-modules", "other/thing", "--add-reads", "foo/bar", "--add-reads", "other/thing", "--patch-module", "foo/bar", "--patch-module", "other/thing" ) // format: on val javaOptions = maybeBuild.toOption.get.options.javaOptions.javaOpts.toSeq.map(_.value.value) expect(javaOptions == expectedOptions) } } // Issue #607 test("-source:future not internally duplicating") { val inputs = TestInputs( os.rel / "foo.scala" -> """//> using option -source:future |def foo = "bar" |""".stripMargin ) inputs.withBuild(defaultOptions, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => val expectedOptions = Seq("-source:future") val scalacOptions = maybeBuild.orThrow.options.scalaOptions.scalacOptions.toSeq.map(_.value.value) expect(scalacOptions == expectedOptions) } } // Issue #525 test("scalac options not spuriously duplicating") { val inputs = TestInputs( os.rel / "foo.scala" -> """//> using scala 2.13 |//> using options -deprecation -feature -Xmaxwarns 1 |//> using option -Xdisable-assertions | |def foo = "bar" |""".stripMargin ) inputs.withBuild(defaultOptions, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => val expectedOptions = Seq("-deprecation", "-feature", "-Xmaxwarns", "1", "-Xdisable-assertions") val scalacOptions = maybeBuild.toOption.get.options.scalaOptions.scalacOptions.toSeq.map(_.value.value) expect(scalacOptions == expectedOptions) } } test("multiple times scalac options with -Xplugin prefix") { val inputs = TestInputs( os.rel / "foo.scala" -> """//> using option -Xplugin:/paradise_2.12.15-2.1.1.jar |//> using option -Xplugin:/semanticdb-scalac_2.12.15-4.4.31.jar | |def foo = "bar" |""".stripMargin ) inputs.withBuild(defaultOptions, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => val expectedOptions = Seq( "-Xplugin:/paradise_2.12.15-2.1.1.jar", "-Xplugin:/semanticdb-scalac_2.12.15-4.4.31.jar" ) val scalacOptions = maybeBuild.toOption.get.options.scalaOptions.scalacOptions.toSeq.map(_.value.value) expect(scalacOptions == expectedOptions) } } test("Pin Scala 2 artifacts version") { val inputs = TestInputs( os.rel / "Foo.scala" -> """//> using dep com.lihaoyi:ammonite_2.13.8:2.5.1-6-5fce97fb |//> using scala 2.13.5 | |object Foo { | def main(args: Array[String]): Unit = { | println(scala.util.Properties.versionNumberString) | } |} |""".stripMargin ) inputs.withBuild(baseOptions, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => expect(maybeBuild.exists(_.success)) val build = maybeBuild.toOption.flatMap(_.successfulOpt).getOrElse(sys.error("cannot happen")) val cp = build.artifacts.classPath.map(_.last) val scalaLibraryJarNameOpt = cp.find(n => n.startsWith("scala-library-") && n.endsWith(".jar")) val scalaCompilerJarNameOpt = cp.find(n => n.startsWith("scala-compiler-") && n.endsWith(".jar")) val scalaReflectJarNameOpt = cp.find(n => n.startsWith("scala-reflect-") && n.endsWith(".jar")) expect(scalaLibraryJarNameOpt.contains("scala-library-2.13.5.jar")) expect(scalaCompilerJarNameOpt.contains("scala-compiler-2.13.5.jar")) expect(scalaReflectJarNameOpt.contains("scala-reflect-2.13.5.jar")) } } test("Pin Scala 3 artifacts version") { val inputs = TestInputs( os.rel / "Foo.scala" -> """//> using dep com.lihaoyi:ammonite_3.1.1:2.5.1-6-5fce97fb |//> using scala 3.1.0 | |object Foo { | def main(args: Array[String]): Unit = { | println(scala.util.Properties.versionNumberString) | } |} |""".stripMargin ) inputs.withBuild(baseOptions, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => expect(maybeBuild.exists(_.success)) val build = maybeBuild.toOption.flatMap(_.successfulOpt).getOrElse(sys.error("cannot happen")) val cp = build.artifacts.classPath.map(_.last) val scalaLibraryJarNameOpt = cp.find(n => n.startsWith("scala3-library_3-") && n.endsWith(".jar")) val scalaCompilerJarNameOpt = cp.find(n => n.startsWith("scala3-compiler_3-") && n.endsWith(".jar")) expect(scalaLibraryJarNameOpt.contains("scala3-library_3-3.1.0.jar")) expect(scalaCompilerJarNameOpt.contains("scala3-compiler_3-3.1.0.jar")) } } test("Pure Java") { val inputs = TestInputs( os.rel / "Foo.java" -> """package foo; | |public class Foo { | public static void main(String[] args) { | System.out.println("Hello"); | } |} |""".stripMargin ) inputs.withBuild(baseOptions, buildThreads, bloopConfigOpt, buildTests = false) { (_, _, maybeBuild) => expect(maybeBuild.exists(_.success)) val build = maybeBuild .toOption .flatMap(_.successfulOpt) .getOrElse(sys.error("cannot happen")) val cp = build.fullClassPath expect(cp.length == 1) // no scala-library, only the class directory } } test("No stubs JAR at runtime") { val inputs = TestInputs( os.rel / "Foo.scala" -> """package foo | |object Foo { | def main(args: Array[String]): Unit = | println("Hello") |} |""".stripMargin ) inputs.withBuild(baseOptions, buildThreads, bloopConfigOpt, buildTests = false) { (_, _, maybeBuild) => expect(maybeBuild.exists(_.success)) val build = maybeBuild .toOption .flatMap(_.successfulOpt) .getOrElse(sys.error("cannot happen")) val cp = build.fullClassPath val coreCp = cp.filter { f => val name = f.last !name.startsWith("scala-library") && !name.startsWith("scala3-library") && !name.startsWith("runner") } expect(coreCp.length == 1) // only classes directory, no stubs jar } } test("declared sources in using directive should be included to count project hash") { val helloFile = "Hello.scala" val inputs = TestInputs( os.rel / helloFile -> """|//> using file Utils.scala | |object Hello extends App { | println(Utils.hello) |}""".stripMargin, os.rel / "Utils.scala" -> s"""|object Utils { | val hello = "Hello" |}""".stripMargin, os.rel / "Helper.scala" -> s"""|object Helper { | val hello = "Hello" |}""".stripMargin ) inputs.withBuild(baseOptions, buildThreads, bloopConfigOpt) { (root, _, maybeBuild) => expect(maybeBuild.exists(_.success)) val build = maybeBuild.toOption.flatMap(_.successfulOpt).getOrElse(sys.error("cannot happen")) // updating sources in using directive should change project name val updatedHelloScala = """|//> using file Helper.scala | |object Hello extends App { | println(Helper.hello) |}""".stripMargin os.write.over(root / helloFile, updatedHelloScala) inputs.withBuild(baseOptions, buildThreads, bloopConfigOpt) { (_, _, maybeUpdatedBuild) => expect(maybeUpdatedBuild.exists(_.success)) val updatedBuild = maybeUpdatedBuild.toOption.flatMap(_.successfulOpt).getOrElse(sys.error("cannot happen")) // project name should be change after updating source in using directive expect(build.inputs.projectName != updatedBuild.inputs.projectName) } } } for (options <- Seq(defaultOptions, defaultScala3Options)) test(s"compile 12k sources for Scala ${options.scalaOptions.scalaVersion.get.asString}") { val mainInput = os.rel / "main.sc" -> s"""//> using jvm ${scala.build.internal.Constants.scala38MinJavaVersion} |println("Hello from big build") |""".stripMargin val additionalInputs = 1 to 12000 map { i => os.rel / s"Foo$i.scala" -> s"""object Foo$i |""".stripMargin } val allInputs = mainInput +: additionalInputs val testInputs = TestInputs(allInputs*) testInputs.withBuild(options, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => expect(maybeBuild.exists(_.success)) } } for (dirValue <- Seq("default", "typelevel:default")) test(s"error when toolkit $dirValue is used with Scala 2.12") { val testInputs = TestInputs( os.rel / "simple.sc" -> s"""//> using toolkit $dirValue | |val n = 2 |println(s"n=$$n") |""".stripMargin ) val scala212Options = baseOptions.copy( scalaOptions = baseOptions.scalaOptions.copy( scalaVersion = Some(MaybeScalaVersion(Constants.defaultScala212Version)), scalaBinaryVersion = None ), scriptOptions = ScriptOptions(Some(true)) ) testInputs.withBuild(scala212Options, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => expect(maybeBuild.left.exists(_.message.startsWith("Toolkits do not support Scala 2.12"))) } } for { (modeStr, bloopMode) <- Seq("fastLinkJs" -> LinkerMode.Debug, "fullLinkJs" -> LinkerMode.Release) if server } do { test(s"bloop config for $modeStr") { val testInputs = TestInputs( os.rel / "Simple.scala" -> """//> using platform js |def foo(): String = "foo" |""".stripMargin ) val jsLinkBuildOptions = defaultOptions.copy( scalaOptions = defaultOptions.scalaOptions.copy( scalaVersion = None ), scalaJsOptions = defaultOptions.scalaJsOptions.copy( mode = ScalaJsMode(Some(modeStr)) ) ) testInputs.withBuild(jsLinkBuildOptions, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => maybeBuild match { case Right(b: Build.Successful) => assert(b.project.scalaJsOptions.exists(_.mode == bloopMode)) case _ => fail("Build failed") } } } test(s"bloop config for noOpt and $modeStr") { val testInputs = TestInputs( os.rel / "Simple.scala" -> """//> using platform js |def foo(): String = "foo" |""".stripMargin ) val noOptBuildOptions = defaultOptions.copy( scalaOptions = defaultOptions.scalaOptions.copy( scalaVersion = None ), scalaJsOptions = defaultOptions.scalaJsOptions.copy( mode = ScalaJsMode(Some(modeStr)), noOpt = Some(true) ) ) testInputs.withBuild(noOptBuildOptions, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => maybeBuild match { case Right(b: Build.Successful) => assert(b.project.scalaJsOptions.exists(_.mode == LinkerMode.Debug)) case _ => fail("Build failed") } } } } } ================================================ FILE: modules/build/src/test/scala/scala/build/tests/BuildTestsBloop.scala ================================================ package scala.build.tests class BuildTestsBloop extends BuildTests(server = true) ================================================ FILE: modules/build/src/test/scala/scala/build/tests/BuildTestsScalac.scala ================================================ package scala.build.tests class BuildTestsScalac extends BuildTests(server = false) { test("warn about Java files in mixed compilation with --server=false") { val recordingLogger = new RecordingLogger() val inputs = TestInputs( os.rel / "Side.java" -> """public class Side { | public static String message = "Hello"; |} |""".stripMargin, os.rel / "Main.scala" -> """@main def main() = println(Side.message) |""".stripMargin ) val options = defaultScala3Options.copy(useBuildServer = Some(false)) inputs.withBuild(options, buildThreads, bloopConfigOpt, logger = Some(recordingLogger)) { (_, _, maybeBuild) => assert(maybeBuild.isRight) val hasWarning = recordingLogger.messages.exists { msg => msg.contains(".java files are not compiled to .class files") && msg.contains("--server=false") && msg.contains("Affected .java files") } assert( hasWarning, s"Expected warning about Java files with --server=false in: ${recordingLogger.messages.mkString("\n")}" ) } } } ================================================ FILE: modules/build/src/test/scala/scala/build/tests/DirectiveTests.scala ================================================ package scala.build.tests import bloop.rifle.BloopRifleConfig import com.eed3si9n.expecty.Expecty.expect import scala.build.Ops.EitherThrowOps import scala.build.errors.{ CompositeBuildException, DependencyFormatError, FetchingDependenciesError, ToolkitDirectiveMissingVersionError } import scala.build.options.{ BuildOptions, InternalOptions, MaybeScalaVersion, ScalaOptions, ScalacOpt, Scope } import scala.build.tests.util.BloopServer import scala.build.{Build, BuildThreads, Directories, LocalRepo, Position, Positioned} class DirectiveTests extends TestUtil.ScalaCliBuildSuite { val buildThreads: BuildThreads = BuildThreads.create() def bloopConfigOpt: Option[BloopRifleConfig] = Some(BloopServer.bloopConfig) val extraRepoTmpDir: os.Path = os.temp.dir(prefix = "scala-cli-tests-extra-repo-") val directories: Directories = Directories.under(extraRepoTmpDir) override def afterAll(): Unit = { TestInputs.tryRemoveAll(extraRepoTmpDir) buildThreads.shutdown() } val baseOptions = BuildOptions( internal = InternalOptions( localRepository = LocalRepo.localRepo(directories.localRepoDir, TestLogger()), keepDiagnostics = true ) ) test("resolving position of dep directive") { val testInputs = TestInputs( os.rel / "simple.sc" -> """//> using dep com.lihaoyi::utest:0.7.10 |""".stripMargin ) testInputs.withBuild(baseOptions, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => val build = maybeBuild.orThrow val dep = build.options.classPathOptions.extraDependencies.toSeq.headOption assert(dep.nonEmpty) val position = dep.get.positions.headOption assert(position.nonEmpty) val (startPos, endPos) = position.get match { case Position.File(_, startPos, endPos, _) => (startPos, endPos) case _ => sys.error("cannot happen") } expect(startPos == (0, 14)) expect(endPos == (0, 39)) } } test("should parse javac options") { val testInputs = TestInputs( os.rel / "simple.sc" -> """//> using javacOpt source 1.8 target 1.8 |""".stripMargin ) testInputs.withBuild(baseOptions, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => val build = maybeBuild.orThrow val javacOpt = build.options.javaOptions.javacOptions val expectedJavacOpt = Seq("source", "1.8", "target", "1.8") expect(javacOpt.map(_.value) == expectedJavacOpt) } } test("should parse graalVM args") { val expectedGraalVMArgs @ Seq(noFallback, enableUrl) = Seq("--no-fallback", "--enable-url-protocols=http,https") TestInputs( os.rel / "simple.sc" -> s"""//> using packaging.graalvmArgs $noFallback $enableUrl |""".stripMargin ).withBuild(baseOptions, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => val build = maybeBuild.orThrow val graalvmArgs = build.options.notForBloopOptions.packageOptions.nativeImageOptions.graalvmArgs expect(graalvmArgs.map(_.value) == expectedGraalVMArgs) } } test(s"resolve toolkit & toolkit-test dependency with version passed") { val testInputs = TestInputs( os.rel / "simple.sc" -> s"""//> using toolkit latest |""".stripMargin ) testInputs.withBuilds(baseOptions, buildThreads, bloopConfigOpt) { (_, _, maybeBuilds) => val expectedVersion = "latest.release" val builds = maybeBuilds.orThrow val mainBuild = builds.get(Scope.Main).get val toolkitDep = mainBuild.options.classPathOptions.extraDependencies.toSeq.headOption.map(_.value).get expect(toolkitDep.organization == Constants.toolkitOrganization) expect(toolkitDep.name == Constants.toolkitName) expect(toolkitDep.version == expectedVersion) val testBuild = builds.get(Scope.Test).get val toolkitTestDep = testBuild.options.classPathOptions.extraDependencies.toSeq.headOption.map(_.value).get expect(toolkitTestDep.organization == Constants.toolkitOrganization) expect(toolkitTestDep.name == Constants.toolkitTestName) expect(toolkitTestDep.version == expectedVersion) } } for (toolkitDirectiveKey <- Seq("toolkit", "test.toolkit")) test(s"missing $toolkitDirectiveKey version produces an informative error message") { val testInputs = TestInputs( os.rel / "simple.sc" -> s"""//> using $toolkitDirectiveKey |""".stripMargin ) testInputs.withBuilds(baseOptions, buildThreads, bloopConfigOpt) { (_, _, maybeBuilds) => maybeBuilds match case Left(ToolkitDirectiveMissingVersionError(_, errorKey)) => expect(errorKey == toolkitDirectiveKey) case _ => sys.error("should not happen") } } for (scope <- Scope.all) { def withProjectFile[T](projectFileContent: String)(f: (Build, Boolean) => T): T = TestInputs( os.rel / "project.scala" -> projectFileContent, os.rel / "Tests.test.scala" -> """class Tests extends munit.FunSuite { | test("foo") { | println("foo") | } |} |""".stripMargin ).withBuild(baseOptions, buildThreads, bloopConfigOpt, scope = scope) { (_, _, maybeBuild) => f(maybeBuild.orThrow, scope == Scope.Test) } test(s"resolve test scope dependencies correctly when building for ${scope.name} scope") { withProjectFile(projectFileContent = """//> using dep com.lihaoyi::os-lib:0.9.1 |//> using test.dep org.scalameta::munit::0.7.29 |""".stripMargin) { (build, isTestScope) => val deps = build.options.classPathOptions.extraDependencies.toSeq.map(_.value) expect(deps.nonEmpty) val hasMainDeps = deps.exists(d => d.organization == "com.lihaoyi" && d.name == "os-lib" && d.version == "0.9.1" ) val hasTestDeps = deps.exists(d => d.organization == "org.scalameta" && d.name == "munit" && d.version == "0.7.29" ) expect(hasMainDeps) expect(if isTestScope then hasTestDeps else !hasTestDeps) } } test(s"resolve test scope javacOpts correctly when building for ${scope.name} scope") { withProjectFile(projectFileContent = """//> using javacOpt source 1.8 |//> using test.javacOpt target 1.8 |//> using test.dep org.scalameta::munit::0.7.29 |""".stripMargin ) { (build, isTestScope) => val javacOpts = build.options.javaOptions.javacOptions.map(_.value) expect(javacOpts.contains("source")) val hasTestJavacOpts = javacOpts.contains("target") expect(if isTestScope then hasTestJavacOpts else !hasTestJavacOpts) } } test(s"resolve test scope scalac opts correctly when building for ${scope.name} scope") { withProjectFile(projectFileContent = """//> using option --explain |//> using test.option -deprecation |//> using test.dep org.scalameta::munit::0.7.29 |""".stripMargin ) { (build, isTestScope) => val scalacOpts = build.options.scalaOptions.scalacOptions.toSeq.map(_.value.value) expect(scalacOpts.contains("--explain")) val hasTestScalacOpts = scalacOpts.contains("-deprecation") expect(if isTestScope then hasTestScalacOpts else !hasTestScalacOpts) } } test(s"resolve test scope javaOpts correctly when building for ${scope.name} scope") { withProjectFile(projectFileContent = """//> using javaOpt -Xmx2g |//> using test.javaOpt -Dsomething=a |//> using test.dep org.scalameta::munit::0.7.29 |""".stripMargin ) { (build, isTestScope) => val javaOpts = build.options.javaOptions.javaOpts.toSeq.map(_.value.value) expect(javaOpts.contains("-Xmx2g")) val hasTestJavaOpts = javaOpts.contains("-Dsomething=a") expect(if isTestScope then hasTestJavaOpts else !hasTestJavaOpts) } } test(s"resolve test scope javaProps correctly when building for ${scope.name} scope") { withProjectFile(projectFileContent = """//> using javaProp foo=1 |//> using test.javaProp bar=2 |//> using test.dep org.scalameta::munit::0.7.29 |""".stripMargin ) { (build, isTestScope) => val javaProps = build.options.javaOptions.javaOpts.toSeq.map(_.value.value) expect(javaProps.contains("-Dfoo=1")) val hasTestJavaProps = javaProps.contains("-Dbar=2") expect(if isTestScope then hasTestJavaProps else !hasTestJavaProps) } } test(s"resolve test scope resourceDir correctly when building for ${scope.name} scope") { withProjectFile(projectFileContent = """//> using resourceDir ./mainResources |//> using test.resourceDir ./testResources |//> using test.dep org.scalameta::munit::0.7.29 |""".stripMargin ) { (build, isTestScope) => val resourcesDirs = build.options.classPathOptions.resourcesDir expect(resourcesDirs.exists(_.last == "mainResources")) val hasTestResources = resourcesDirs.exists(_.last == "testResources") expect(if isTestScope then hasTestResources else !hasTestResources) } } test(s"resolve test scope toolkit dependency correctly when building for ${scope.name} scope") { withProjectFile( projectFileContent = s"""//> using test.toolkit ${Constants.toolkitVersion} |""".stripMargin ) { (build, isTestScope) => val deps = build.options.classPathOptions.extraDependencies.toSeq.map(_.value) if isTestScope then expect(deps.nonEmpty) val hasToolkitDep = deps.exists(d => d.organization == Constants.toolkitOrganization && d.name == Constants.toolkitName && d.version == Constants.toolkitVersion ) val hasTestToolkitDep = deps.exists(d => d.organization == Constants.toolkitOrganization && d.name == Constants.toolkitTestName && d.version == Constants.toolkitVersion ) expect(if isTestScope then hasToolkitDep else !hasToolkitDep) expect(if isTestScope then hasTestToolkitDep else !hasTestToolkitDep) } } } test("handling special syntax for path") { val filePath = os.rel / "src" / "simple.scala" val testInputs = TestInputs( os.rel / filePath -> """//> using options -coverage-out:${.}""" ) testInputs.withBuild(baseOptions, buildThreads, bloopConfigOpt) { (root, _, maybeBuild) => val build = maybeBuild.orThrow val scalacOptions: Option[Positioned[ScalacOpt]] = build.options.scalaOptions.scalacOptions.toSeq.headOption assert(scalacOptions.nonEmpty) val scalacOpt = scalacOptions.get.value.value val expectedCoveragePath = (root / filePath / os.up).toString expect(scalacOpt == s"-coverage-out:$expectedCoveragePath") } } test("handling special syntax for path with more dollars before") { val filePath = os.rel / "src" / "simple.scala" val testInputs = TestInputs( os.rel / filePath -> """//> using options -coverage-out:$$${.}""" ) testInputs.withBuild(baseOptions, buildThreads, bloopConfigOpt) { (root, _, maybeBuild) => val build = maybeBuild.orThrow val scalacOptions: Option[Positioned[ScalacOpt]] = build.options.scalaOptions.scalacOptions.toSeq.headOption assert(scalacOptions.nonEmpty) val scalacOpt = scalacOptions.get.value.value val expectedCoveragePath = (root / filePath / os.up).toString expect(scalacOpt == s"-coverage-out:$$$expectedCoveragePath") } } test("skip handling special syntax for path when double dollar") { val filePath = os.rel / "src" / "simple.scala" val testInputs = TestInputs( os.rel / filePath -> """//> using options -coverage-out:$${.}""" ) testInputs.withBuild(baseOptions, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => val build = maybeBuild.orThrow val scalacOptions: Option[Positioned[ScalacOpt]] = build.options.scalaOptions.scalacOptions.toSeq.headOption assert(scalacOptions.nonEmpty) val scalacOpt = scalacOptions.get.value.value expect(scalacOpt == """-coverage-out:${.}""") } } test("resolve typelevel toolkit dependency") { val testInputs = TestInputs( os.rel / "simple.sc" -> """//> using toolkit typelevel:latest |""".stripMargin ) testInputs.withBuild(baseOptions, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => val build = maybeBuild.orThrow val dep = build.options.classPathOptions.extraDependencies.toSeq.headOption assert(dep.nonEmpty) val toolkitDep = dep.get.value expect(toolkitDep.organization == Constants.typelevelToolkitOrganization) expect(toolkitDep.name == Constants.toolkitName) expect(toolkitDep.version == "latest.release") } } def testSourceJar(getDirectives: (String, String) => String): Unit = { val dummyJar = "Dummy.jar" val dummySourcesJar = "Dummy-sources.jar" TestInputs( os.rel / "Main.scala" -> s"""${getDirectives(dummyJar, dummySourcesJar)} |object Main extends App { | println("Hello") |} |""".stripMargin, os.rel / dummyJar -> "dummy", os.rel / dummySourcesJar -> "dummy-sources" ).withBuild(baseOptions, buildThreads, bloopConfigOpt) { (root, _, maybeBuild) => val build = maybeBuild.orThrow val jar = build.options.classPathOptions.extraClassPath.head expect(jar == root / dummyJar) val sourceJar = build.options.classPathOptions.extraSourceJars.head expect(sourceJar == root / dummySourcesJar) } } test("source jar") { testSourceJar((dummyJar, dummySourcesJar) => s"""//> using jar $dummyJar |//> using sourceJar $dummySourcesJar""".stripMargin ) } test("assumed source jar") { testSourceJar((dummyJar, dummySourcesJar) => s"//> using jars $dummyJar $dummySourcesJar" ) } test("include test.resourceDir into sources for test scope") { val testInputs = TestInputs( os.rel / "simple.sc" -> """//> using test.resourceDir foo |""".stripMargin ) testInputs.withBuild(baseOptions, buildThreads, bloopConfigOpt, scope = Scope.Test) { (root, _, maybeBuild) => val build = maybeBuild.toOption.flatMap(_.successfulOpt).getOrElse(sys.error("cannot happen")) val resourceDirs = build.sources.resourceDirs expect(resourceDirs.nonEmpty) expect(resourceDirs.length == 1) val path = root / "foo" expect(resourceDirs == Seq(path)) } } test("do not include test.resourceDir into sources for main scope") { val testInputs = TestInputs( os.rel / "simple.sc" -> """//> using test.resourceDir foo |""".stripMargin ) testInputs.withBuild(baseOptions, buildThreads, bloopConfigOpt, scope = Scope.Main) { (_, _, maybeBuild) => val build = maybeBuild.toOption.flatMap(_.successfulOpt).getOrElse(sys.error("cannot happen")) val resourceDirs = build.sources.resourceDirs expect(resourceDirs.isEmpty) } } test("parse boolean for publish.doc") { val testInputs = TestInputs( os.rel / "simple.sc" -> """//> using publish.doc false |""".stripMargin ) testInputs.withBuild(baseOptions, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => val build = maybeBuild.orThrow val publishOptionsCI = build.options.notForBloopOptions.publishOptions.contextual(isCi = true) val publishOptionsLocal = build.options.notForBloopOptions.publishOptions.contextual(isCi = false) expect(publishOptionsCI.docJar.contains(false)) expect(publishOptionsLocal.docJar.contains(false)) } } test("dependency parsing error with position") { val testInputs = TestInputs( os.rel / "simple.sc" -> """//> using dep not-a-dep |""".stripMargin ) testInputs.withBuild(baseOptions, buildThreads, bloopConfigOpt) { (root, _, maybeBuild) => expect(maybeBuild.isLeft) val error = maybeBuild.left.toOption.get error match { case error: DependencyFormatError => expect( error.message == "Error parsing dependency 'not-a-dep': malformed module: not-a-dep" ) expect(error.positions.length == 1) val path = root / "simple.sc" expect(error.positions.head == Position.File( Right(path), (0, 14), (0, 23) )) case _ => fail("unexpected BuildException type") } } } test("separate dependency resolution errors for each dependency") { val testInputs = TestInputs( os.rel / "simple.sc" -> """//> using dep org.xyz::foo:0.0.1 |//> using dep com.lihaoyi::os-lib:0.9.1 org.qwerty::bar:0.0.1 |""".stripMargin ) testInputs.withBuild(baseOptions, buildThreads, bloopConfigOpt) { (root, _, maybeBuild) => expect(maybeBuild.isLeft) val errors = maybeBuild.left.toOption.get errors match { case error: CompositeBuildException => expect(error.exceptions.length == 2) expect(error.exceptions.forall { case _: FetchingDependenciesError => true case _ => false }) expect(error.exceptions.forall(_.positions.length == 1)) { val xyzError = error.exceptions.find(_.message.contains("org.xyz")).get expect(xyzError.message.startsWith("Error downloading org.xyz:foo")) expect(!xyzError.message.contains("com.lihaoyi")) expect(!xyzError.message.contains("org.qwerty")) val path = root / "simple.sc" expect(xyzError.positions.head == Position.File( Right(path), (0, 14), (0, 32) )) } { val qwertyError = error.exceptions.find(_.message.contains("org.qwerty")).get expect(qwertyError.message.contains("Error downloading org.qwerty:bar")) expect(!qwertyError.message.contains("com.lihaoyi")) expect(!qwertyError.message.contains("org.xyz")) val path = root / "simple.sc" expect(qwertyError.positions.head == Position.File( Right(path), (1, 40), (1, 61) )) } case _ => fail("unexpected BuildException type") } } } test("main scope dependencies propagate to test scope") { val Scala322Options = baseOptions.copy(scalaOptions = ScalaOptions( scalaVersion = Some(MaybeScalaVersion("3.2.2")) ) ) val testInputs = TestInputs( os.rel / "simple.sc" -> """//> using target.scala 3.2.2 |//> using dep com.lihaoyi::os-lib:0.9.1 |""".stripMargin, os.rel / "test" / "test.sc" -> """println(os.list(os.pwd)) |""".stripMargin ) testInputs.withBuild(Scala322Options, buildThreads, bloopConfigOpt, scope = Scope.Test) { (_, _, maybeBuild) => expect(maybeBuild.exists(_.success)) } } } ================================================ FILE: modules/build/src/test/scala/scala/build/tests/DistinctByTests.scala ================================================ package scala.build.tests import com.eed3si9n.expecty.Expecty.expect class DistinctByTests extends TestUtil.ScalaCliBuildSuite { case class Message(a: String, b: Int) val distinctData: Seq[Message] = Seq( Message(a = "1", b = 4), Message(a = "2", b = 3), Message(a = "3", b = 2), Message(a = "4", b = 1) ) val repeatingData: Seq[Message] = Seq( Message(a = "1", b = 4), Message(a = "1", b = 44), Message(a = "2", b = 3), Message(a = "22", b = 3), Message(a = "3", b = 22), Message(a = "33", b = 2), Message(a = "4", b = 1), Message(a = "4", b = 11) ) test("distinctBy where data is already distinct") { val distinctByA = distinctData.distinctBy(_.a) val distinctByB = distinctData.distinctBy(_.b) val generalDistinct = distinctData.distinct expect(distinctData == generalDistinct) expect(distinctData == distinctByA) expect(distinctData == distinctByB) } test("distinctBy doesn't change data order") { val expectedData = Seq( Message(a = "1", b = 4), Message(a = "2", b = 3), Message(a = "22", b = 3), Message(a = "3", b = 22), Message(a = "33", b = 2), Message(a = "4", b = 1) ) expect(repeatingData.distinctBy(_.a) == expectedData) } } ================================================ FILE: modules/build/src/test/scala/scala/build/tests/ExcludeTests.scala ================================================ package scala.build.tests import com.eed3si9n.expecty.Expecty.expect import coursier.cache.Cache.Fetch import coursier.cache.{ArchiveCache, ArtifactError, Cache} import coursier.util.{Artifact, EitherT, Task} import java.io.File import scala.build.Ops.* import scala.build.errors.ExcludeDefinitionError import scala.build.input.ScalaCliInvokeData import scala.build.options.{BuildOptions, Scope, SuppressWarningOptions} import scala.build.preprocessing.Preprocessor import scala.build.{CrossSources, Sources} import scala.concurrent.ExecutionContext class ExcludeTests extends TestUtil.ScalaCliBuildSuite { val preprocessors: Seq[Preprocessor] = Sources.defaultPreprocessors( archiveCache = ArchiveCache().withCache( new Cache[Task] { def fetch: Fetch[Task] = _ => sys.error("shouldn't be used") def file(artifact: Artifact): EitherT[Task, ArtifactError, File] = sys.error("shouldn't be used") def ec: ExecutionContext = sys.error("shouldn't be used") } ), javaClassNameVersionOpt = None, javaCommand = () => sys.error("shouldn't be used") ) test("throw error when exclude found in multiple files") { val testInputs = TestInputs( os.rel / "Hello.scala" -> """//> using exclude *.sc |""".stripMargin, os.rel / "Main.scala" -> """//> using exclude */test/* |""".stripMargin ) testInputs.withInputs { (_, inputs) => val crossSources = CrossSources.forInputs( inputs, preprocessors, TestLogger(), SuppressWarningOptions() )(using ScalaCliInvokeData.dummy) crossSources match { case Left(_: ExcludeDefinitionError) => case o => fail("Exception expected", clues(o)) } } } test("throw error when exclude found in non top-level project.scala and file") { val testInputs = TestInputs( os.rel / "Main.scala" -> """//> using exclude */test/* |""".stripMargin, os.rel / "src" / "project.scala" -> s"""//> using exclude *.sc""" ) testInputs.withInputs { (_, inputs) => val crossSources = CrossSources.forInputs( inputs, preprocessors, TestLogger(), SuppressWarningOptions() )(using ScalaCliInvokeData.dummy) crossSources match { case Left(_: ExcludeDefinitionError) => case o => fail("Exception expected", clues(o)) } } } test("multiple excludes") { val testInputs = TestInputs( os.rel / "Hello.scala" -> "object Hello", os.rel / "World.scala" -> "object World", os.rel / "Main.scala" -> "object Main", os.rel / "project.scala" -> s"""//> using exclude Hello.scala World.scala""" ) testInputs.withInputs { (root, inputs) => val (crossSources, _) = CrossSources.forInputs( inputs, preprocessors, TestLogger(), SuppressWarningOptions() )(using ScalaCliInvokeData.dummy).orThrow val scopedSources = crossSources.scopedSources(BuildOptions()) .orThrow val sources = scopedSources.sources( Scope.Main, crossSources.sharedOptions(BuildOptions()), root, TestLogger() ) .orThrow expect(sources.paths.nonEmpty) expect(sources.paths.length == 2) val paths = Seq(os.rel / "Main.scala", os.rel / "project.scala") expect(sources.paths.map(_._2) == paths) } } test("exclude relative paths") { val testInputs = TestInputs( os.rel / "Hello.scala" -> "object Hello", os.rel / "Main.scala" -> """object Main { |}""".stripMargin, os.rel / "project.scala" -> s"""//> using exclude Main.scala""" ) testInputs.withInputs { (root, inputs) => val (crossSources, _) = CrossSources.forInputs( inputs, preprocessors, TestLogger(), SuppressWarningOptions() )(using ScalaCliInvokeData.dummy).orThrow val scopedSources = crossSources.scopedSources(BuildOptions()) .orThrow val sources = scopedSources.sources( Scope.Main, crossSources.sharedOptions(BuildOptions()), root, TestLogger() ) .orThrow expect(sources.paths.nonEmpty) expect(sources.paths.length == 2) val paths = Seq(os.rel / "Hello.scala", os.rel / "project.scala") expect(sources.paths.map(_._2) == paths) } } test("exclude absolute file paths") { val testInputs = TestInputs( os.rel / "Hello.scala" -> "object Hello", os.rel / "Main.scala" -> """object Main { |}""".stripMargin, os.rel / "project.scala" -> s"""//> using exclude $${.}${File.separator}Main.scala""" ) testInputs.withInputs { (root, inputs) => val (crossSources, _) = CrossSources.forInputs( inputs, preprocessors, TestLogger(), SuppressWarningOptions() )(using ScalaCliInvokeData.dummy).orThrow val scopedSources = crossSources.scopedSources(BuildOptions()) .orThrow val sources = scopedSources.sources( Scope.Main, crossSources.sharedOptions(BuildOptions()), root, TestLogger() ) .orThrow expect(sources.paths.nonEmpty) expect(sources.paths.length == 2) val paths = Seq(os.rel / "Hello.scala", os.rel / "project.scala") expect(sources.paths.map(_._2) == paths) } } test("exclude relative directory paths") { val testInputs = TestInputs( os.rel / "Hello.scala" -> "object Hello", os.rel / "src" / "scala" / "Main.scala" -> """object Main { |}""".stripMargin, os.rel / "project.scala" -> """//> using exclude src/*.scala""" ) testInputs.withInputs { (root, inputs) => val (crossSources, _) = CrossSources.forInputs( inputs, preprocessors, TestLogger(), SuppressWarningOptions() )(using ScalaCliInvokeData.dummy).orThrow val scopedSources = crossSources.scopedSources(BuildOptions()) .orThrow val sources = scopedSources.sources( Scope.Main, crossSources.sharedOptions(BuildOptions()), root, TestLogger() ) .orThrow expect(sources.paths.nonEmpty) expect(sources.paths.length == 2) val paths = Seq(os.rel / "Hello.scala", os.rel / "project.scala") expect(sources.paths.map(_._2) == paths) } } test("exclude relative directory paths with glob pattern") { val testInputs = TestInputs( os.rel / "Hello.scala" -> "object Hello", os.rel / "src" / "scala" / "Main.scala" -> """object Main { |}""".stripMargin, os.rel / "project.scala" -> """//> using exclude src/*.scala""" ) testInputs.withInputs { (root, inputs) => val (crossSources, _) = CrossSources.forInputs( inputs, preprocessors, TestLogger(), SuppressWarningOptions() )(using ScalaCliInvokeData.dummy).orThrow val scopedSources = crossSources.scopedSources(BuildOptions()) .orThrow val sources = scopedSources.sources( Scope.Main, crossSources.sharedOptions(BuildOptions()), root, TestLogger() ) .orThrow expect(sources.paths.nonEmpty) expect(sources.paths.length == 2) val paths = Seq(os.rel / "Hello.scala", os.rel / "project.scala") expect(sources.paths.map(_._2) == paths) } } } ================================================ FILE: modules/build/src/test/scala/scala/build/tests/FrameworkDiscoveryTests.scala ================================================ package scala.build.tests import java.nio.file.Files import scala.build.errors.NoFrameworkFoundByNativeBridgeError import scala.build.testrunner.{AsmTestRunner, Logger as TestRunnerLogger} class FrameworkDiscoveryTests extends TestUtil.ScalaCliBuildSuite { test( "findFrameworkServices parses Java ServiceLoader format (trim, skip comments and empty lines)" ) { val dir = Files.createTempDirectory("scala-cli-framework-services-") try { val servicesDir = dir.resolve("META-INF").resolve("services") Files.createDirectories(servicesDir) val serviceFile = servicesDir.resolve("sbt.testing.Framework") // Content with newlines, comments, and surrounding whitespace val content = """munit.Framework |# comment line | | munit.native.Framework | |""".stripMargin Files.writeString(serviceFile, content) val found = AsmTestRunner.findFrameworkServices(Seq(dir), TestRunnerLogger(0)) assertEquals( found.sorted, Seq("munit.Framework", "munit.native.Framework"), clue = "Service file lines should be trimmed; comments and empty lines skipped" ) } finally { def deleteRecursively(p: java.nio.file.Path): Unit = { if Files.isDirectory(p) then Files.list(p).forEach(deleteRecursively) Files.deleteIfExists(p) } deleteRecursively(dir) } } test("NoFrameworkFoundByNativeBridgeError has Native-specific message (not Scala.js)") { val err = new NoFrameworkFoundByNativeBridgeError assert(err.getMessage.contains("Scala Native"), clue = "Message should mention Scala Native") assert(!err.getMessage.contains("Scala.js"), clue = "Message should not mention Scala.js") } } ================================================ FILE: modules/build/src/test/scala/scala/build/tests/InputsTests.scala ================================================ package scala.build.tests import bloop.rifle.BloopRifleConfig import com.eed3si9n.expecty.Expecty.expect import scala.build.input.* import scala.build.input.ElementsUtils.* import scala.build.internal.Constants import scala.build.options.{BuildOptions, InternalOptions} import scala.build.tests.util.BloopServer import scala.build.{Build, BuildThreads, Directories, LocalRepo} class InputsTests extends TestUtil.ScalaCliBuildSuite { val buildThreads: BuildThreads = BuildThreads.create() val extraRepoTmpDir: os.Path = os.temp.dir(prefix = "scala-cli-tests-extra-repo-") val directories: Directories = Directories.under(extraRepoTmpDir) def bloopConfigOpt: Option[BloopRifleConfig] = Some(BloopServer.bloopConfig) val buildOptions: BuildOptions = BuildOptions( internal = InternalOptions( localRepository = LocalRepo.localRepo(directories.localRepoDir, TestLogger()), keepDiagnostics = true ) ) test("forced workspace") { val testInputs = TestInputs( os.rel / "Foo.scala" -> """object Foo { | def main(): Unit = { | println("Hello") | } |} |""".stripMargin ) val forcedWorkspace = os.rel / "workspace" testInputs.withCustomInputs(viaDirectory = false, forcedWorkspaceOpt = Some(forcedWorkspace)) { (root, inputs) => expect(inputs.workspace == root / forcedWorkspace) expect(inputs.baseProjectName == "workspace") } } test("project file") { val projectFileName = Constants.projectFileName val testInputs = TestInputs( files = Seq( os.rel / "custom-dir" / projectFileName -> "", os.rel / projectFileName -> s"//> using javaProp \"foo=bar\"".stripMargin, os.rel / "foo.scala" -> s"""object Foo { | def main(args: Array[String]): Unit = | println("Foo") |} |""".stripMargin ), inputArgs = Seq("foo.scala", "custom-dir", projectFileName) ) testInputs.withBuild(buildOptions, buildThreads, bloopConfigOpt) { (root, _, buildMaybe) => val javaOptsCheck = buildMaybe match { case Right(build: Build.Successful) => build.options.javaOptions.javaOpts.toSeq.head.value.value == "-Dfoo=bar" case _ => false } assert(javaOptsCheck) assert(os.exists(root / "custom-dir" / Constants.workspaceDirName)) assert(!os.exists(root / Constants.workspaceDirName)) val filesUnderScalaBuild = os.list(root / "custom-dir" / Constants.workspaceDirName) assert(filesUnderScalaBuild.exists(_.baseName.startsWith("custom-dir"))) assert(!filesUnderScalaBuild.exists(_.baseName.startsWith("project"))) } } test("setting root dir without project settings file") { val testInputs = TestInputs( files = Seq( os.rel / "custom-dir" / "foo.scala" -> s"""object Foo { | def main(args: Array[String]): Unit = | println("Foo") |} |""".stripMargin, os.rel / "bar.scala" -> "" ), inputArgs = Seq("custom-dir", "bar.scala") ) testInputs.withBuild(buildOptions, buildThreads, bloopConfigOpt) { (root, _, _) => assert(os.exists(root / "custom-dir" / Constants.workspaceDirName)) assert(!os.exists(root / Constants.workspaceDirName)) val filesUnderScalaBuild = os.list(root / "custom-dir" / Constants.workspaceDirName) assert(filesUnderScalaBuild.exists(_.baseName.startsWith("custom-dir"))) assert(!filesUnderScalaBuild.exists(_.baseName.startsWith("project"))) } } test("passing project file and its parent directory") { val projectFileName = Constants.projectFileName val testInputs = TestInputs( files = Seq( os.rel / "foo.scala" -> s"""object Foo { | def main(args: Array[String]): Unit = | println("Foo") |} |""".stripMargin, os.rel / projectFileName -> "" ), inputArgs = Seq(".", projectFileName) ) testInputs.withBuild(buildOptions, buildThreads, bloopConfigOpt) { (root, inputs, _) => assert(os.exists(root / Constants.workspaceDirName)) assert(inputs.elements.projectSettingsFiles.length == 1) val filesUnderScalaBuild = os.list(root / Constants.workspaceDirName) assert(filesUnderScalaBuild.exists(_.baseName.startsWith(root.baseName))) assert(!filesUnderScalaBuild.exists(_.baseName.startsWith("project"))) } } test("sbt file is recognized as SbtFile when passed explicitly") { TestInputs(os.rel / "build.sbt" -> "").fromRoot { root => val elements = Inputs.validateArgs( Seq((root / "build.sbt").toString), root, download = _ => Right(Array.emptyByteArray), stdinOpt = None, acceptFds = false, enableMarkdown = false )(using ScalaCliInvokeData.dummy) elements match { case Seq(Right(Seq(f: SbtFile))) => assert(f.path == root / "build.sbt") case _ => fail(s"Unexpected elements: $elements") } } } test("sbt file is picked up from directory scan") { TestInputs(os.rel / "build.sbt" -> "").fromRoot { root => val dir = Directory(root) val singles = dir.singleFilesFromDirectory(enableMarkdown = false) val sbtFiles = singles.collect { case f: SbtFile => f } assert(sbtFiles.nonEmpty) assert(sbtFiles.head.path == root / "build.sbt") } } test("URLs with query parameters") { val urlBase = "https://gist.githubusercontent.com/USER/hash/raw/hash" val urls = Seq( s"$urlBase/test.sc", s"$urlBase/test.sc?foo=bar", s"$urlBase/test.sc?foo=endsWith.md", s"http://gist.githubusercontent.com/USER/hash/raw/hash/test.sc?foo=bar", s"$urlBase/test.scala?foo=endsWith.java", s"$urlBase/test.java?token=123456789123456789", s"file:///Users/user/content/test.sc" ) TestInputs().fromRoot { root => val elements = Inputs.validateArgs( urls, root, download = _ => Right(Array.emptyByteArray), stdinOpt = None, acceptFds = true, enableMarkdown = true )(using ScalaCliInvokeData.dummy) elements match { case Seq( Right(Seq(el1: VirtualScript)), Right(Seq(el2: VirtualScript)), Right(Seq(el3: VirtualScript)), Right(Seq(el4: VirtualScript)), Right(Seq(el5: VirtualScalaFile)), Right(Seq(el6: VirtualJavaFile)), Right(Seq(el7: VirtualScript)) ) => Seq(el1, el2, el3, el4, el5, el6, el7) .zip(urls) .foreach { case (el: VirtualScript, url) => expect(el.source == url) expect(el.content.isEmpty) val path = os.rel / "test.sc" expect(el.wrapperPath.endsWith(path)) case (el: VirtualScalaFile, url) => expect(el.source == url) expect(el.content.isEmpty) case (el: VirtualJavaFile, url) => expect(el.source == url) expect(el.content.isEmpty) case _ => fail("Unexpected elements") } case _ => fail("Unexpected elements") } } } } ================================================ FILE: modules/build/src/test/scala/scala/build/tests/JavaTestRunnerTests.scala ================================================ package scala.build.tests import com.eed3si9n.expecty.Expecty.assert as expect import scala.build.options.* class JavaTestRunnerTests extends TestUtil.ScalaCliBuildSuite { private def makeOptions( scalaVersionOpt: Option[MaybeScalaVersion], addTestRunner: Boolean ): BuildOptions = BuildOptions( scalaOptions = ScalaOptions( scalaVersion = scalaVersionOpt ), internalDependencies = InternalDependenciesOptions( addTestRunnerDependencyOpt = Some(addTestRunner) ) ) test("pure Java build has no scalaParams") { val opts = makeOptions(Some(MaybeScalaVersion.none), addTestRunner = false) val params = opts.scalaParams.toOption.flatten expect(params.isEmpty, "Pure Java build should have no scalaParams") } test("Scala build has scalaParams") { val opts = makeOptions(None, addTestRunner = false) val params = opts.scalaParams.toOption.flatten expect(params.isDefined, "Scala build should have scalaParams") } test("pure Java test build gets addJvmJavaTestRunner=true in Artifacts params") { val opts = makeOptions(Some(MaybeScalaVersion.none), addTestRunner = true) val isJava = opts.scalaParams.toOption.flatten.isEmpty expect(isJava, "Expected pure Java build to have no scalaParams") } test("Scala test build gets addJvmTestRunner=true in Artifacts params") { val opts = makeOptions(None, addTestRunner = true) val isJava = opts.scalaParams.toOption.flatten.isEmpty expect(!isJava, "Expected Scala build to have scalaParams") } test("mixed Scala+Java build still gets Scala test runner") { val opts = makeOptions(None, addTestRunner = true) val isJava = opts.scalaParams.toOption.flatten.isEmpty expect(!isJava, "Mixed Scala+Java build should still use Scala test runner") } } ================================================ FILE: modules/build/src/test/scala/scala/build/tests/OfflineTests.scala ================================================ package scala.build.tests import coursier.cache.FileCache import scala.build.errors.ScalaVersionError import scala.build.options.{BuildOptions, InternalOptions} import scala.build.tests.Constants import scala.build.{BuildThreads, Directories} class OfflineTests extends TestUtil.ScalaCliBuildSuite { val extraRepoTmpDir: os.Path = os.temp.dir(prefix = "scala-cli-tests-offline-") val directories: Directories = Directories.under(extraRepoTmpDir) val baseOptions: BuildOptions = BuildOptions( internal = InternalOptions( cache = Some(FileCache() .withLocation(directories.cacheDir.toString) .withCachePolicies(Seq(coursier.cache.CachePolicy.LocalOnly))) ) ) val buildThreads: BuildThreads = BuildThreads.create() for ( defaultVersion <- Seq( Constants.defaultScalaVersion, Constants.defaultScala212Version, Constants.defaultScala213Version ) ) test(s"Default versions of Scala should pass without validation for $defaultVersion") { val testInputs = TestInputs( os.rel / "script.sc" -> s"""//> using scala $defaultVersion |def msg: String = "Hello" | |println(msg) |""".stripMargin ) testInputs.withBuild(baseOptions, buildThreads, None) { (_, _, maybeBuild) => maybeBuild match { case Left(e: ScalaVersionError) => munit.Assertions.fail( s"Validation Failed with:${System.lineSeparator()} ${e.getMessage}" ) case _ => () } } } } ================================================ FILE: modules/build/src/test/scala/scala/build/tests/PackagingUsingDirectiveTests.scala ================================================ package scala.build.tests import bloop.rifle.BloopRifleConfig import com.eed3si9n.expecty.Expecty.expect import scala.build.options.{BuildOptions, InternalOptions, PackageType} import scala.build.tests.util.BloopServer import scala.build.{BuildThreads, Directories, LocalRepo} class PackagingUsingDirectiveTests extends TestUtil.ScalaCliBuildSuite { val buildThreads: BuildThreads = BuildThreads.create() def bloopConfig: Option[BloopRifleConfig] = Some(BloopServer.bloopConfig) val extraRepoTmpDir: os.Path = os.temp.dir(prefix = "scala-cli-tests-extra-repo-") val directories: Directories = Directories.under(extraRepoTmpDir) val buildOptions = BuildOptions( internal = InternalOptions( localRepository = LocalRepo.localRepo(directories.localRepoDir, TestLogger()), keepDiagnostics = true ) ) test("package type") { val inputs = TestInputs( os.rel / "p.sc" -> """//> using packaging.packageType graalvm |def foo() = println("hello foo") |""".stripMargin ) inputs.withLoadedBuild(buildOptions, buildThreads, bloopConfig) { (_, _, maybeBuild) => val foundPackageTypeOpt = maybeBuild.options.notForBloopOptions.packageOptions.packageTypeOpt expect(foundPackageTypeOpt.contains(PackageType.GraalVMNativeImage)) } } test("output") { val output = "foo" val inputs = TestInputs( os.rel / "Bar.scala" -> s"""//> using packaging.output $output |def hello() = println("hello") |""".stripMargin ) inputs.withLoadedBuild(buildOptions, buildThreads, bloopConfig) { (_, _, maybeBuild) => val maybePackageOutput = maybeBuild.options.notForBloopOptions.packageOptions.output val packageOutputString = maybePackageOutput.getOrElse("None") val index = packageOutputString.lastIndexOf('/') val packageName = packageOutputString.drop(index + 1) expect(packageName == output) } } test("docker options") { val inputs = TestInputs( os.rel / "p.sc" -> """//> using packaging.dockerFrom openjdk:11 |//> using packaging.dockerImageTag 1.0.0 |//> using packaging.dockerImageRegistry virtuslab |//> using packaging.dockerImageRepository scala-cli | |def foo() = println("hello foo") |""".stripMargin ) inputs.withLoadedBuild(buildOptions, buildThreads, bloopConfig) { (_, _, maybeBuild) => val dockerOpt = maybeBuild.options.notForBloopOptions.packageOptions.dockerOptions expect(dockerOpt.from == Some("openjdk:11")) expect(dockerOpt.imageTag == Some("1.0.0")) expect(dockerOpt.imageRegistry == Some("virtuslab")) expect(dockerOpt.imageRepository == Some("scala-cli")) } } } ================================================ FILE: modules/build/src/test/scala/scala/build/tests/PreprocessingTests.scala ================================================ package scala.build.tests import com.eed3si9n.expecty.Expecty.expect import scala.build.input.{MarkdownFile, ScalaCliInvokeData, Script, SourceScalaFile} import scala.build.options.SuppressWarningOptions import scala.build.preprocessing.{MarkdownPreprocessor, ScalaPreprocessor, ScriptPreprocessor} class PreprocessingTests extends TestUtil.ScalaCliBuildSuite { test("Report error if scala file not exists") { val logger = TestLogger() val scalaFile = SourceScalaFile(os.temp.dir(), os.SubPath("NotExists.scala")) val res = ScalaPreprocessor.preprocess( scalaFile, logger, allowRestrictedFeatures = false, suppressWarningOptions = SuppressWarningOptions() )(using ScalaCliInvokeData.dummy) val expectedMessage = s"File not found: ${scalaFile.path}" assert(res.nonEmpty) assert(res.get.isLeft) expect(res.get.swap.toOption.get.message == expectedMessage) } test("Report error if scala script not exists") { val logger = TestLogger() val scalaScript = Script(os.temp.dir(), os.SubPath("NotExists.sc"), None) val res = ScriptPreprocessor.preprocess( scalaScript, logger, allowRestrictedFeatures = false, suppressWarningOptions = SuppressWarningOptions() )(using ScalaCliInvokeData.dummy) val expectedMessage = s"File not found: ${scalaScript.path}" assert(res.nonEmpty) assert(res.get.isLeft) expect(res.get.swap.toOption.get.message == expectedMessage) } test("Report error if markdown does not exist") { val logger = TestLogger() val markdownFile = MarkdownFile(os.temp.dir(), os.SubPath("NotExists.md")) val res = MarkdownPreprocessor.preprocess( markdownFile, logger, allowRestrictedFeatures = false, suppressWarningOptions = SuppressWarningOptions() )(using ScalaCliInvokeData.dummy) val expectedMessage = s"File not found: ${markdownFile.path}" assert(res.nonEmpty) assert(res.get.isLeft) expect(res.get.swap.toOption.get.message == expectedMessage) } } ================================================ FILE: modules/build/src/test/scala/scala/build/tests/ReplArtifactsTests.scala ================================================ package scala.build.tests import com.eed3si9n.expecty.Expecty.expect import coursier.cache.FileCache import dependency.ScalaParameters import scala.build.{Logger, ReplArtifacts} class ReplArtifactsTests extends TestUtil.ScalaCliBuildSuite { def scalaPyTest(version: String, usesFormerOrg: Boolean = false): Unit = TestInputs.withTmpDir("replartifactstests") { _ => val artifacts = ReplArtifacts.ammonite( scalaParams = ScalaParameters("2.13.8"), ammoniteVersion = "2.5.4", dependencies = Nil, extraClassPath = Nil, extraSourceJars = Nil, extraRepositories = Nil, logger = Logger.nop, cache = FileCache(), addScalapy = Some(version) ).fold(e => throw new Exception(e), identity) val urls = artifacts.replArtifacts.map(_._1) val meShadajUrls = urls.filter(_.startsWith("https://repo1.maven.org/maven2/me/shadaj/")) val devScalaPyUrls = urls.filter(_.startsWith("https://repo1.maven.org/maven2/dev/scalapy/")) if (usesFormerOrg) { expect(meShadajUrls.nonEmpty) expect(devScalaPyUrls.isEmpty) } else { expect(meShadajUrls.isEmpty) expect(devScalaPyUrls.nonEmpty) } } test("ScalaPy former organization") { scalaPyTest("0.5.2+5-83f1eb68", usesFormerOrg = true) } test("ScalaPy new organization") { scalaPyTest("0.5.2+9-623f0807") } } ================================================ FILE: modules/build/src/test/scala/scala/build/tests/ScalaNativeUsingDirectiveTests.scala ================================================ package scala.build.tests import bloop.rifle.BloopRifleConfig import com.eed3si9n.expecty.Expecty.expect import scala.build.errors.UsingDirectiveValueNumError import scala.build.options.{BuildOptions, InternalOptions} import scala.build.tests.util.BloopServer import scala.build.{BuildThreads, Directories, LocalRepo} class ScalaNativeUsingDirectiveTests extends TestUtil.ScalaCliBuildSuite { val buildThreads: BuildThreads = BuildThreads.create() def bloopConfig: Option[BloopRifleConfig] = Some(BloopServer.bloopConfig) val extraRepoTmpDir: os.Path = os.temp.dir(prefix = "scala-cli-tests-extra-repo-") val directories: Directories = Directories.under(extraRepoTmpDir) val buildOptions = BuildOptions( internal = InternalOptions( localRepository = LocalRepo.localRepo(directories.localRepoDir, TestLogger()), keepDiagnostics = true ) ) test("ScalaNativeOptions for native-gc with no values") { val inputs = TestInputs( os.rel / "p.sc" -> """//> using `native-gc` |def foo() = println("hello foo") |""".stripMargin ) inputs.withBuild(buildOptions, buildThreads, bloopConfig) { (_, _, maybeBuild) => expect( maybeBuild.left.exists { case _: UsingDirectiveValueNumError => true; case _ => false } ) } } test("ScalaNativeOptions for native-gc with multiple values") { val inputs = TestInputs( os.rel / "p.sc" -> """//> using native-gc 78 12 |def foo() = println("hello foo") |""".stripMargin ) inputs.withBuild(buildOptions, buildThreads, bloopConfig) { (_, _, maybeBuild) => assert( maybeBuild.left.exists { case _: UsingDirectiveValueNumError => true; case _ => false } ) } } test("ScalaNativeOptions for native-gc") { val inputs = TestInputs( os.rel / "p.sc" -> """//> using native-gc 78 |def foo() = println("hello foo") |""".stripMargin ) inputs.withLoadedBuild(buildOptions, buildThreads, bloopConfig) { (_, _, maybeBuild) => val gcStr = maybeBuild.options.scalaNativeOptions.gcStr expect(gcStr.contains("78")) } } test("ScalaNativeOptions for native-mode with no values") { val inputs = TestInputs( os.rel / "p.sc" -> """//> using `native-mode` |def foo() = println("hello foo") |""".stripMargin ) inputs.withBuild(buildOptions, buildThreads, bloopConfig) { (_, _, maybeBuild) => expect( maybeBuild.left.exists { case _: UsingDirectiveValueNumError => true; case _ => false } ) } } test("ScalaNativeOptions for native-mode with multiple values") { val inputs = TestInputs( os.rel / "p.sc" -> """//> using native-mode debug release-full |def foo() = println("hello foo") |""".stripMargin ) inputs.withBuild(buildOptions, buildThreads, bloopConfig) { (_, _, maybeBuild) => assert( maybeBuild.left.exists { case _: UsingDirectiveValueNumError => true; case _ => false } ) } } test("ScalaNativeOptions for native-mode") { val inputs = TestInputs( os.rel / "p.sc" -> """//> using native-mode release-full |def foo() = println("hello foo") |""".stripMargin ) inputs.withLoadedBuild(buildOptions, buildThreads, bloopConfig) { (_, _, maybeBuild) => assert(maybeBuild.options.scalaNativeOptions.modeStr.get == "release-full") } } test("ScalaNativeOptions for native-version with multiple values") { val inputs = TestInputs( os.rel / "p.sc" -> """//> using native-version 0.4.0 0.3.3 |def foo() = println("hello foo") |""".stripMargin ) inputs.withBuild(buildOptions, buildThreads, bloopConfig) { (_, _, maybeBuild) => assert( maybeBuild.left.exists { case _: UsingDirectiveValueNumError => true; case _ => false } ) } } test("ScalaNativeOptions for native-version") { val inputs = TestInputs( os.rel / "p.sc" -> """//> using native-version 0.4.0 |def foo() = println("hello foo") |""".stripMargin ) inputs.withLoadedBuild(buildOptions, buildThreads, bloopConfig) { (_, _, maybeBuild) => assert(maybeBuild.options.scalaNativeOptions.version.get == "0.4.0") } } test("ScalaNativeOptions for native-compile") { val inputs = TestInputs( os.rel / "p.sc" -> """//> using native-compile compileOption1 compileOption2 |def foo() = println("hello foo") |""".stripMargin ) inputs.withLoadedBuild(buildOptions, buildThreads, bloopConfig) { (_, _, maybeBuild) => assert( maybeBuild.options.scalaNativeOptions.compileOptions.head == "compileOption1" ) assert( maybeBuild.options.scalaNativeOptions.compileOptions(1) == "compileOption2" ) } } for { directiveKey <- Seq("nativeCCompile", "native-c-compile") } test(s"ScalaNativeOptions for $directiveKey") { val expectedOption1 = "compileOption1" val expectedOption2 = "compileOption2" val inputs = TestInputs( os.rel / "p.sc" -> s"""//> using $directiveKey $expectedOption1 $expectedOption2 |def foo() = println("hello foo") |""".stripMargin ) inputs.withLoadedBuild(buildOptions, buildThreads, bloopConfig) { (_, _, maybeBuild) => assert( maybeBuild.options.scalaNativeOptions.cCompileOptions.head == expectedOption1 ) assert( maybeBuild.options.scalaNativeOptions.cCompileOptions.drop(1).head == expectedOption2 ) } } for { directiveKey <- Seq("nativeCppCompile", "native-cpp-compile") } test(s"ScalaNativeOptions for $directiveKey") { val expectedOption1 = "compileOption1" val expectedOption2 = "compileOption2" val inputs = TestInputs( os.rel / "p.sc" -> s"""//> using $directiveKey $expectedOption1 $expectedOption2 |def foo() = println("hello foo") |""".stripMargin ) inputs.withLoadedBuild(buildOptions, buildThreads, bloopConfig) { (_, _, maybeBuild) => assert( maybeBuild.options.scalaNativeOptions.cppCompileOptions.head == expectedOption1 ) assert( maybeBuild.options.scalaNativeOptions.cppCompileOptions.drop(1).head == expectedOption2 ) } } test("ScalaNativeOptions for native-linking and no value") { val inputs = TestInputs( os.rel / "p.sc" -> """//> using `native-linking` |def foo() = println("hello foo") |""".stripMargin ) inputs.withBuild(buildOptions, buildThreads, bloopConfig) { (_, _, maybeBuild) => assert(maybeBuild.exists { build => build.options.scalaNativeOptions.linkingOptions.isEmpty }) } } test("ScalaNativeOptions for native-linking") { val inputs = TestInputs( os.rel / "p.sc" -> """//> using native-linking linkingOption1 linkingOption2 |def foo() = println("hello foo") |""".stripMargin ) inputs.withLoadedBuild(buildOptions, buildThreads, bloopConfig) { (_, _, maybeBuild) => assert( maybeBuild.options.scalaNativeOptions.linkingOptions.head == "linkingOption1" ) assert( maybeBuild.options.scalaNativeOptions.linkingOptions(1) == "linkingOption2" ) } } test("ScalaNativeOptions for native-clang") { val inputs = TestInputs( os.rel / "p.sc" -> """//> using native-clang clang/path |def foo() = println("hello foo") |""".stripMargin ) inputs.withLoadedBuild(buildOptions, buildThreads, bloopConfig) { (_, _, maybeBuild) => assert( maybeBuild.options.scalaNativeOptions.clang.get == "clang/path" ) } } test("ScalaNativeOptions for native-clang and multiple values") { val inputs = TestInputs( os.rel / "p.sc" -> """//> using native-clang path1 path2 |def foo() = println("hello foo") |""".stripMargin ) inputs.withBuild(buildOptions, buildThreads, bloopConfig) { (_, _, maybeBuild) => assert( maybeBuild.left.exists { case _: UsingDirectiveValueNumError => true; case _ => false } ) } } test("ScalaNativeOptions for native-clang-pp") { val inputs = TestInputs( os.rel / "p.sc" -> """//> using native-clang-pp clangpp/path |def foo() = println("hello foo") |""".stripMargin ) inputs.withLoadedBuild(buildOptions, buildThreads, bloopConfig) { (_, _, maybeBuild) => assert( maybeBuild.options.scalaNativeOptions.clangpp.get == "clangpp/path" ) } } test("ScalaNativeOptions for native-clang-pp and multiple values") { val inputs = TestInputs( os.rel / "p.sc" -> """//> using native-clang-pp path1 path2 |def foo() = println("hello foo") |""".stripMargin ) inputs.withBuild(buildOptions, buildThreads, bloopConfig) { (_, _, maybeBuild) => assert( maybeBuild.left.exists { case _: UsingDirectiveValueNumError => true; case _ => false } ) } } for { multithreadingDirective <- Seq("`native-multithreading`", "nativeMultithreading") } test(s"ScalaNativeOptions for $multithreadingDirective") { val inputs = TestInputs( os.rel / "p.sc" -> s"""//> using $multithreadingDirective |def foo() = println("hello foo") |""".stripMargin ) inputs.withLoadedBuild(buildOptions, buildThreads, bloopConfig) { (_, _, maybeBuild) => assert(maybeBuild.options.scalaNativeOptions.multithreading.get) } } } ================================================ FILE: modules/build/src/test/scala/scala/build/tests/ScalaPreprocessorTests.scala ================================================ package scala.build.tests import com.eed3si9n.expecty.Expecty.expect import scala.build.input.{ScalaCliInvokeData, Script, SourceScalaFile} import scala.build.options.SuppressWarningOptions import scala.build.preprocessing.{ScalaPreprocessor, ScriptPreprocessor} class ScalaPreprocessorTests extends TestUtil.ScalaCliBuildSuite { test("should respect using directives in a .scala file with the shebang line") { val lastUsingLine = "//> using dep \"com.lihaoyi::os-lib::0.8.1\" \"com.lihaoyi::os-lib::0.8.1\"" TestInputs(os.rel / "Main.scala" -> s"""#!/usr/bin/env -S scala-cli shebang |//> using jvm 11 |$lastUsingLine | |object Main { | def main(args: Array[String]): Unit = { | println(os.pwd) | } |}""".stripMargin).fromRoot { root => val scalaFile = SourceScalaFile(root, os.sub / "Main.scala") val result = ScalaPreprocessor.preprocess( scalaFile, logger = TestLogger(), allowRestrictedFeatures = true, suppressWarningOptions = SuppressWarningOptions() )(using ScalaCliInvokeData.dummy).get.getOrElse(sys.error("preprocessing failed")) expect(result.nonEmpty) val directivesPositions = result.head.directivesPositions.get expect(directivesPositions.startPos == 0 -> 0) expect(directivesPositions.endPos == 3 -> lastUsingLine.length) } } test("should respect using directives in a .sc file with the shebang line") { val depLine = "//> using dep com.lihaoyi::os-lib::0.8.1" TestInputs(os.rel / "sample.sc" -> s"""#!/usr/bin/env -S scala-cli shebang |$depLine |println(os.pwd) |""".stripMargin).fromRoot { root => val scalaFile = Script(root, os.sub / "sample.sc", None) val result = ScriptPreprocessor.preprocess( scalaFile, logger = TestLogger(), allowRestrictedFeatures = false, suppressWarningOptions = SuppressWarningOptions() )(using ScalaCliInvokeData.dummy).get.getOrElse(sys.error("preprocessing failed")) expect(result.nonEmpty) val directivesPositions = result.head.directivesPositions.get expect(directivesPositions.startPos == 0 -> 0) expect(directivesPositions.endPos == 2 -> depLine.length) } } val lastUsingLines: Seq[(String, String)] = Seq( "//> using dep com.lihaoyi::os-lib::0.8.1 com.lihaoyi::os-lib::0.8.1" -> "string literal", "//> using scala 2.13.7" -> "numerical string", "//> using objectWrapper true" -> "boolean literal", "//> using objectWrapper" -> "empty value literal" ) for ((lastUsingLine, typeName) <- lastUsingLines) do test(s"correct directive positions with $typeName") { TestInputs(os.rel / "Main.scala" -> s"""#!/usr/bin/env -S scala-cli shebang |//> using jvm 11 |$lastUsingLine | |object Main { | def main(args: Array[String]): Unit = { | println(os.pwd) | } |}""".stripMargin).fromRoot { root => val scalaFile = SourceScalaFile(root, os.sub / "Main.scala") val result = ScalaPreprocessor.preprocess( scalaFile, logger = TestLogger(), allowRestrictedFeatures = true, suppressWarningOptions = SuppressWarningOptions() )(using ScalaCliInvokeData.dummy).get.getOrElse(sys.error("preprocessing failed")) expect(result.nonEmpty) val directivesPositions = result.head.directivesPositions.get expect(directivesPositions.startPos == 0 -> 0) expect(directivesPositions.endPos == 3 -> lastUsingLine.length) } } } ================================================ FILE: modules/build/src/test/scala/scala/build/tests/ScriptWrapperTests.scala ================================================ package scala.build.tests import bloop.rifle.BloopRifleConfig import com.eed3si9n.expecty.Expecty.expect import scala.build.Ops.EitherThrowOps import scala.build.options.{ BuildOptions, InternalOptions, MaybeScalaVersion, Platform, ScalaOptions, ScriptOptions } import scala.build.tests.util.BloopServer import scala.build.{Build, BuildThreads, Directories, LocalRepo, Position, Positioned} class ScriptWrapperTests extends TestUtil.ScalaCliBuildSuite { def expectAppWrapper(wrapperName: String, path: os.Path): Unit = { val generatedFileContent = os.read(path) assert( generatedFileContent.contains(s"object $wrapperName extends App {"), clue(s"Generated file content: $generatedFileContent") ) assert( !generatedFileContent.contains(s"final class $wrapperName$$_") && !generatedFileContent.contains(s"object $wrapperName {"), clue(s"Generated file content: $generatedFileContent") ) } def expectObjectWrapper(wrapperName: String, path: os.Path): Unit = { val generatedFileContent = os.read(path) assert( generatedFileContent.contains(s"object $wrapperName {"), clue(s"Generated file content: $generatedFileContent") ) assert( !generatedFileContent.contains(s"final class $wrapperName$$_") && !generatedFileContent.contains(s"object $wrapperName extends App {"), clue(s"Generated file content: $generatedFileContent") ) } def expectClassWrapper(wrapperName: String, path: os.Path): Unit = { val generatedFileContent = os.read(path) assert( generatedFileContent.contains(s"final class $wrapperName$$_"), clue(s"Generated file content: $generatedFileContent") ) assert( !generatedFileContent.contains(s"object $wrapperName extends App {") && !generatedFileContent.contains(s"object $wrapperName {"), clue(s"Generated file content: $generatedFileContent") ) } val buildThreads: BuildThreads = BuildThreads.create() def bloopConfigOpt: Option[BloopRifleConfig] = Some(BloopServer.bloopConfig) val extraRepoTmpDir: os.Path = os.temp.dir(prefix = "scala-cli-tests-extra-repo-") val directories: Directories = Directories.under(extraRepoTmpDir) override def afterAll(): Unit = { TestInputs.tryRemoveAll(extraRepoTmpDir) buildThreads.shutdown() } val baseOptions = BuildOptions( internal = InternalOptions( localRepository = LocalRepo.localRepo(directories.localRepoDir, TestLogger()), keepDiagnostics = true ) ) val objectWrapperOptions = BuildOptions( scriptOptions = ScriptOptions( forceObjectWrapper = Some(true) ) ) val scala213Options = BuildOptions( scalaOptions = ScalaOptions( scalaVersion = Some(MaybeScalaVersion(Some("2.13"))) ) ) val platfromJsOptions = BuildOptions( scalaOptions = ScalaOptions( platform = Some(Positioned(List(Position.CommandLine()), Platform.JS)) ) ) test(s"class wrapper for scala 3") { val inputs = TestInputs( os.rel / "script1.sc" -> s"""//> using dep com.lihaoyi::os-lib:0.9.1 | |def main(args: String*): Unit = println("Hello") |main() |""".stripMargin, os.rel / "script2.sc" -> """//> using dep com.lihaoyi::os-lib:0.9.1 | |println("Hello") |""".stripMargin ) inputs.withBuild(baseOptions, buildThreads, bloopConfigOpt) { (root, _, maybeBuild) => expect(maybeBuild.orThrow.success) val projectDir = os.list(root / ".scala-build").filter( _.baseName.startsWith(root.baseName + "_") ) expect(projectDir.size == 1) expectClassWrapper( "script1", projectDir.head / "src_generated" / "main" / "script1.scala" ) expectClassWrapper( "script2", projectDir.head / "src_generated" / "main" / "script2.scala" ) } } for { useDirectives <- Seq(true, false) (directive, options, optionName) <- Seq( ("//> using object.wrapper", objectWrapperOptions, "--object-wrapper"), ("//> using platform js", platfromJsOptions, "--js") ) } { val inputs = TestInputs( os.rel / "script1.sc" -> s"""//> using dep com.lihaoyi::os-lib:0.9.1 |${if (useDirectives) directive else ""} | |def main(args: String*): Unit = println("Hello") |main() |""".stripMargin, os.rel / "script2.sc" -> """//> using dep com.lihaoyi::os-lib:0.9.1 | |println("Hello") |""".stripMargin ) test( s"object wrapper forced with ${if (useDirectives) directive else optionName}" ) { inputs.withBuild(options.orElse(baseOptions), buildThreads, bloopConfigOpt) { (root, _, maybeBuild) => expect(maybeBuild.orThrow.success) val projectDir = os.list(root / ".scala-build").filter( _.baseName.startsWith(root.baseName + "_") ) expect(projectDir.size == 1) expectObjectWrapper( "script1", projectDir.head / "src_generated" / "main" / "script1.scala" ) expectObjectWrapper( "script2", projectDir.head / "src_generated" / "main" / "script2.scala" ) } } } for { useDirectives <- Seq(true, false) (directive, options, optionName) <- Seq( ("//> using scala 2.13", scala213Options, "--scala 2.13") ) } { val inputs = TestInputs( os.rel / "script1.sc" -> s"""//> using dep com.lihaoyi::os-lib:0.9.1 |${if (useDirectives) directive else ""} | |def main(args: String*): Unit = println("Hello") |main() |""".stripMargin, os.rel / "script2.sc" -> """//> using dep com.lihaoyi::os-lib:0.9.1 | |println("Hello") |""".stripMargin ) test( s"App object wrapper forced with ${if (useDirectives) directive else optionName}" ) { inputs.withBuild(options.orElse(baseOptions), buildThreads, bloopConfigOpt) { (root, _, maybeBuild) => expect(maybeBuild.orThrow.success) val projectDir = os.list(root / ".scala-build").filter( _.baseName.startsWith(root.baseName + "_") ) expect(projectDir.size == 1) expectAppWrapper( "script1", projectDir.head / "src_generated" / "main" / "script1.scala" ) expectAppWrapper( "script2", projectDir.head / "src_generated" / "main" / "script2.scala" ) } } } for { (targetDirective, enablingDirective) <- Seq( ("target.scala 3.2.2", "scala 3.2.2"), ("target.platform scala-native", "platform scala-native") ) } { val inputs = TestInputs( os.rel / "script1.sc" -> s"""//> using dep com.lihaoyi::os-lib:0.9.1 |//> using $targetDirective |//> using objectWrapper | |def main(args: String*): Unit = println("Hello") |main() |""".stripMargin, os.rel / "script2.sc" -> s"""//> using dep com.lihaoyi::os-lib:0.9.1 |//> using $enablingDirective | |println("Hello") |""".stripMargin ) test( s"object wrapper with $targetDirective" ) { inputs.withBuild(baseOptions, buildThreads, bloopConfigOpt) { (root, _, maybeBuild) => expect(maybeBuild.orThrow.success) val projectDir = os.list(root / ".scala-build").filter( _.baseName.startsWith(root.baseName + "_") ) expect(projectDir.size == 1) expectObjectWrapper( "script1", projectDir.head / "src_generated" / "main" / "script1.scala" ) expectObjectWrapper( "script2", projectDir.head / "src_generated" / "main" / "script2.scala" ) } } } } ================================================ FILE: modules/build/src/test/scala/scala/build/tests/SourceGeneratorTests.scala ================================================ package scala.build.tests import com.eed3si9n.expecty.Expecty.expect import scala.Console.println import scala.build.Ops.EitherThrowOps import scala.build.options.{BuildOptions, InternalOptions} import scala.build.tests.util.BloopServer import scala.build.{Build, BuildThreads, Directories, LocalRepo, Position} class SourceGeneratorTests extends TestUtil.ScalaCliBuildSuite { val buildThreads = BuildThreads.create() def bloopConfigOpt = Some(BloopServer.bloopConfig) val extraRepoTmpDir = os.temp.dir(prefix = "scala-cli-tests-extra-repo-") val directories = Directories.under(extraRepoTmpDir) override def afterAll(): Unit = { TestInputs.tryRemoveAll(extraRepoTmpDir) buildThreads.shutdown() } val baseOptions = BuildOptions( internal = InternalOptions( localRepository = LocalRepo.localRepo(directories.localRepoDir, TestLogger()), keepDiagnostics = true ) ) private def normalizeContents(contents: String): String = contents .replaceAll( "ivy:file:[^\"]*scala-cli-tests-extra-repo[^\"]*/local-repo[^\"]*", "ivy:file:.../scala-cli-tests-extra-repo/local-repo/..." ) .replaceAll( "ivy:file:[^\"]*\\.ivy2/local[^\"]*", "ivy:file:.../.ivy2/local/" ) .replaceAll( "val scalaCliVersion = Some\\(\"[^\"]*\"\\)", "val scalaCliVersion = Some\\(\"1.1.1-SNAPSHOT\"\\)" ) .replaceAll("\\\\", "\\") .linesWithSeparators .filterNot(_.stripLeading().startsWith("/**")) .mkString def initializeGit( cwd: os.Path, tag: String = "test-inputs", gitUserName: String = "testUser", gitUserEmail: String = "testUser@scala-cli-tests.com" ): Unit = { println(s"Initializing git in $cwd...") os.proc("git", "init").call(cwd = cwd) println(s"Setting git user.name to $gitUserName") os.proc("git", "config", "--local", "user.name", gitUserName).call(cwd = cwd) println(s"Setting git user.email to $gitUserEmail") os.proc("git", "config", "--local", "user.email", gitUserEmail).call(cwd = cwd) println(s"Adding $cwd to git...") os.proc("git", "add", ".").call(cwd = cwd) println(s"Doing an initial commit...") os.proc("git", "commit", "-m", "git init test inputs").call(cwd = cwd) println(s"Tagging as $tag...") os.proc("git", "tag", tag).call(cwd = cwd) println(s"Git initialized at $cwd") } test("BuildInfo source generated") { val inputs = TestInputs( os.rel / "main.scala" -> """//> using dep com.lihaoyi::os-lib:0.9.1 |//> using option -Xasync |//> using plugin org.wartremover:::wartremover:3.0.9 |//> using scala 3.2.2 |//> using jvm 11 |//> using mainClass Main |//> using resourceDir ./resources |//> using jar TEST1.jar TEST2.jar | |//> using buildInfo | |import scala.cli.build.BuildInfo | |object Main extends App { | println(s"Scala version: ${BuildInfo.scalaVersion}") | BuildInfo.Main.customJarsDecls.foreach(println) |} |""".stripMargin ) inputs.fromRoot { root => initializeGit(root, "v1.0.0") inputs.copy(forceCwd = Some(root)) .withBuild(baseOptions, buildThreads, bloopConfigOpt, skipCreatingSources = true) { (root, _, maybeBuild) => expect(maybeBuild.orThrow.success) val projectDir = os.list(root / ".scala-build").filter( _.baseName.startsWith(root.baseName + "_") ) expect(projectDir.size == 1) val buildInfoPath = projectDir.head / "src_generated" / "main" / "BuildInfo.scala" expect(os.isFile(buildInfoPath)) val buildInfoContent = os.read(buildInfoPath) assertNoDiff( normalizeContents(buildInfoContent), s"""package scala.cli.build | |object BuildInfo { | val scalaVersion = "3.2.2" | val platform = "JVM" | val jvmVersion = Some("11") | val scalaJsVersion = None | val jsEsVersion = None | val scalaNativeVersion = None | val mainClass = Some("Main") | val projectVersion = Some("1.0.0") | val scalaCliVersion = Some("1.1.1-SNAPSHOT") | | object Main { | val sources = Seq("${root / "main.scala"}") | val scalacOptions = Seq("-Xasync") | val scalaCompilerPlugins = Seq("org.wartremover:wartremover_3.2.2:3.0.9") | val dependencies = Seq("com.lihaoyi:os-lib_3:0.9.1") | val resolvers = Seq("ivy:file:.../scala-cli-tests-extra-repo/local-repo/...", "https://repo1.maven.org/maven2", "ivy:file:.../.ivy2/local/") | val resourceDirs = Seq("${root / "resources"}") | val customJarsDecls = Seq("${root / "TEST1.jar"}", "${root / "TEST2.jar"}") | } | | object Test { | val sources = Nil | val scalacOptions = Nil | val scalaCompilerPlugins = Nil | val dependencies = Nil | val resolvers = Nil | val resourceDirs = Nil | val customJarsDecls = Nil | } |} |""".stripMargin ) } } } test("BuildInfo for native") { val inputs = TestInputs( os.rel / "main.scala" -> s"""//> using dep com.lihaoyi::os-lib:0.9.1 |//> using option -Xasync |//> using plugin org.wartremover:::wartremover:3.0.9 |//> using scala 3.2.2 |//> using jvm 11 |//> using mainClass Main |//> using resourceDir ./resources |//> using jar TEST1.jar TEST2.jar |//> using platform scala-native |//> using nativeVersion 0.4.6 | |//> using buildInfo | |import scala.cli.build.BuildInfo | |object Main extends App { | println(s"Scala version: $${BuildInfo.scalaVersion}") | BuildInfo.Main.customJarsDecls.foreach(println) |} |""".stripMargin ) inputs.withBuild(baseOptions, buildThreads, bloopConfigOpt) { (root, _, maybeBuild) => expect(maybeBuild.orThrow.success) val projectDir = os.list(root / ".scala-build").filter( _.baseName.startsWith(root.baseName + "_") ) expect(projectDir.size == 1) val buildInfoPath = projectDir.head / "src_generated" / "main" / "BuildInfo.scala" expect(os.isFile(buildInfoPath)) val buildInfoContent = os.read(buildInfoPath) assertNoDiff( normalizeContents(buildInfoContent), s"""package scala.cli.build | |object BuildInfo { | val scalaVersion = "3.2.2" | val platform = "Native" | val jvmVersion = None | val scalaJsVersion = None | val jsEsVersion = None | val scalaNativeVersion = Some("0.4.6") | val mainClass = Some("Main") | val projectVersion = None | val scalaCliVersion = Some("1.1.1-SNAPSHOT") | | object Main { | val sources = Seq("${root / "main.scala"}") | val scalacOptions = Seq("-Xasync") | val scalaCompilerPlugins = Seq("org.wartremover:wartremover_3.2.2:3.0.9") | val dependencies = Seq("com.lihaoyi:os-lib_3:0.9.1") | val resolvers = Seq("ivy:file:.../scala-cli-tests-extra-repo/local-repo/...", "https://repo1.maven.org/maven2", "ivy:file:.../.ivy2/local/") | val resourceDirs = Seq("${root / "resources"}") | val customJarsDecls = Seq("${root / "TEST1.jar"}", "${root / "TEST2.jar"}") | } | | object Test { | val sources = Nil | val scalacOptions = Nil | val scalaCompilerPlugins = Nil | val dependencies = Nil | val resolvers = Nil | val resourceDirs = Nil | val customJarsDecls = Nil | } |} |""".stripMargin ) } } test("BuildInfo for js") { val inputs = TestInputs( os.rel / "main.scala" -> s"""//> using dep com.lihaoyi::os-lib:0.9.1 |//> using option -Xasync |//> using plugin org.wartremover:::wartremover:3.0.9 |//> using scala 3.2.2 |//> using jvm 11 |//> using mainClass Main |//> using resourceDir ./resources |//> using jar TEST1.jar TEST2.jar |//> using platform scala-js |//> using jsVersion 1.13.1 |//> using jsEsVersionStr es2015 | |//> using buildInfo | |import scala.cli.build.BuildInfo | |object Main extends App { | println(s"Scala version: $${BuildInfo.scalaVersion}") | BuildInfo.Main.customJarsDecls.foreach(println) |} |""".stripMargin ) inputs.withBuild(baseOptions, buildThreads, bloopConfigOpt) { (root, _, maybeBuild) => expect(maybeBuild.orThrow.success) val projectDir = os.list(root / ".scala-build").filter( _.baseName.startsWith(root.baseName + "_") ) expect(projectDir.size == 1) val buildInfoPath = projectDir.head / "src_generated" / "main" / "BuildInfo.scala" expect(os.isFile(buildInfoPath)) val buildInfoContent = os.read(buildInfoPath) assertNoDiff( normalizeContents(buildInfoContent), s"""package scala.cli.build | |object BuildInfo { | val scalaVersion = "3.2.2" | val platform = "JS" | val jvmVersion = None | val scalaJsVersion = Some("1.13.1") | val jsEsVersion = Some("es2015") | val scalaNativeVersion = None | val mainClass = Some("Main") | val projectVersion = None | val scalaCliVersion = Some("1.1.1-SNAPSHOT") | | object Main { | val sources = Seq("${root / "main.scala"}") | val scalacOptions = Seq("-Xasync") | val scalaCompilerPlugins = Seq("org.wartremover:wartremover_3.2.2:3.0.9") | val dependencies = Seq("com.lihaoyi:os-lib_3:0.9.1") | val resolvers = Seq("ivy:file:.../scala-cli-tests-extra-repo/local-repo/...", "https://repo1.maven.org/maven2", "ivy:file:.../.ivy2/local/") | val resourceDirs = Seq("${root / "resources"}") | val customJarsDecls = Seq("${root / "TEST1.jar"}", "${root / "TEST2.jar"}") | } | | object Test { | val sources = Nil | val scalacOptions = Nil | val scalaCompilerPlugins = Nil | val dependencies = Nil | val resolvers = Nil | val resourceDirs = Nil | val customJarsDecls = Nil | } |} |""".stripMargin ) } } test("BuildInfo for Scala 2") { val inputs = TestInputs( os.rel / "main.scala" -> s"""//> using dep com.lihaoyi::os-lib:0.9.1 |//> using option -Xasync |//> using plugin org.wartremover:::wartremover:3.0.9 |//> using scala 2.13.6 |//> using jvm 11 |//> using mainClass Main |//> using resourceDir ./resources |//> using jar TEST1.jar TEST2.jar | |//> using buildInfo | |import scala.cli.build.BuildInfo | |object Main extends App { | println(s"Scala version: $${BuildInfo.scalaVersion}") | BuildInfo.Main.customJarsDecls.foreach(println) |} |""".stripMargin ) inputs.withBuild(baseOptions, buildThreads, bloopConfigOpt) { (root, _, maybeBuild) => expect(maybeBuild.orThrow.success) val projectDir = os.list(root / ".scala-build").filter( _.baseName.startsWith(root.baseName + "_") ) expect(projectDir.size == 1) val buildInfoPath = projectDir.head / "src_generated" / "main" / "BuildInfo.scala" expect(os.isFile(buildInfoPath)) val buildInfoContent = os.read(buildInfoPath) assertNoDiff( normalizeContents(buildInfoContent), s"""package scala.cli.build | |object BuildInfo { | val scalaVersion = "2.13.6" | val platform = "JVM" | val jvmVersion = Some("11") | val scalaJsVersion = None | val jsEsVersion = None | val scalaNativeVersion = None | val mainClass = Some("Main") | val projectVersion = None | val scalaCliVersion = Some("1.1.1-SNAPSHOT") | | object Main { | val sources = Seq("${root / "main.scala"}") | val scalacOptions = Seq("-Xasync") | val scalaCompilerPlugins = Seq("org.wartremover:wartremover_2.13.6:3.0.9") | val dependencies = Seq("com.lihaoyi:os-lib_2.13:0.9.1") | val resolvers = Seq("ivy:file:.../scala-cli-tests-extra-repo/local-repo/...", "https://repo1.maven.org/maven2", "ivy:file:.../.ivy2/local/") | val resourceDirs = Seq("${root / "resources"}") | val customJarsDecls = Seq("${root / "TEST1.jar"}", "${root / "TEST2.jar"}") | } | | object Test { | val sources = Nil | val scalacOptions = Nil | val scalaCompilerPlugins = Nil | val dependencies = Nil | val resolvers = Nil | val resourceDirs = Nil | val customJarsDecls = Nil | } |} |""".stripMargin ) } } test("BuildInfo no git repository error") { val usingPrefix = "//> using computeVersion \"" val usingValue = "git:tag" val usingSuffix = "\"" val inputs = TestInputs( os.rel / "main.scala" -> s""" |$usingPrefix$usingValue$usingSuffix |//> using buildInfo | |import scala.cli.build.BuildInfo | |object Main extends App { | println(s"Scala version: $${BuildInfo.projectVersion}") |} |""".stripMargin ) inputs.withBuild(baseOptions, buildThreads, bloopConfigOpt) { (root, _, maybeBuild) => maybeBuild match { case Left(buildException) => expect(buildException.positions.size == 1) val position = buildException.positions.head assertEquals( position, scala.build.Position.File( Right(root / "main.scala"), (1, usingPrefix.length), (1, (usingPrefix + usingValue).length) ) ) assertNoDiff( buildException.message, s"BuildInfo generation error: $root doesn't look like a Git repository" ) case _ => fail("Build should fail") } } } } ================================================ FILE: modules/build/src/test/scala/scala/build/tests/SourcesTests.scala ================================================ package scala.build.tests import com.eed3si9n.expecty.Expecty.expect import coursier.cache.Cache.Fetch import coursier.cache.{ArchiveCache, ArtifactError, Cache} import coursier.util.{Artifact, EitherT, Task} import dependency.* import java.io.File import java.nio.charset.StandardCharsets import scala.build.Ops.* import scala.build.errors.{UsingDirectiveValueNumError, UsingDirectiveWrongValueTypeError} import scala.build.input.ScalaCliInvokeData import scala.build.internal.ScalaJsLinkerConfig import scala.build.options.{BuildOptions, Scope, SuppressWarningOptions} import scala.build.preprocessing.Preprocessor import scala.build.{CrossSources, Position, Sources} import scala.concurrent.ExecutionContext class SourcesTests extends TestUtil.ScalaCliBuildSuite { def scalaVersion: String = "2.13.5" def scalaParams: ScalaParameters = ScalaParameters(scalaVersion) def scalaBinaryVersion: String = scalaParams.scalaBinaryVersion given ScalaCliInvokeData = ScalaCliInvokeData.dummy val preprocessors: Seq[Preprocessor] = Sources.defaultPreprocessors( ArchiveCache().withCache( new Cache[Task] { def fetch: Fetch[Task] = _ => sys.error("shouldn't be used") def file(artifact: Artifact): EitherT[Task, ArtifactError, File] = sys.error("shouldn't be used") def ec: ExecutionContext = sys.error("shouldn't be used") } ), None, () => sys.error("shouldn't be used") ) for ( (singularAlias, pluralAlias) <- List(("lib", "libs"), ("dep", "deps"), ("dependency", "dependencies")) ) test(s"dependencies in .scala - using aliases: $pluralAlias and $singularAlias") { val testInputs = TestInputs( os.rel / "something.scala" -> s"""//> using $pluralAlias org1:name1:1.1 org2::name2:2.2 |//> using $singularAlias org3:::name3:3.3 |import scala.collection.mutable | |object Something { | def a = 1 |} |""".stripMargin ) val expectedDeps = Seq( dep"org1:name1:1.1", dep"org2::name2:2.2", dep"org3:::name3:3.3" ) testInputs.withInputs { (root, inputs) => val (crossSources, _) = CrossSources.forInputs( inputs, preprocessors, TestLogger(), SuppressWarningOptions() ).orThrow val scopedSources = crossSources.scopedSources(BuildOptions()).orThrow val sources = scopedSources.sources( Scope.Main, crossSources.sharedOptions(BuildOptions()), root, TestLogger() ) .orThrow val obtainedDeps = sources.buildOptions.classPathOptions.extraDependencies.toSeq.map( _.value ) expect(obtainedDeps.sortBy(_.version) == expectedDeps.sortBy(_.version)) expect(sources.paths.length == 1) val path = os.rel / "something.scala" expect(sources.paths.map(_._2) == Seq(path)) expect(sources.inMemory.isEmpty) } } test("dependencies in .scala - using witin tests") { val testInputs = TestInputs( os.rel / "something.test.scala" -> """//> using deps org1:name1:1.1 org2::name2:2.2 |//> using dep org3:::name3:3.3 |import scala.collection.mutable | |object Something { | def a = 1 |} |""".stripMargin ) val expectedDeps = Nil testInputs.withInputs { (root, inputs) => val (crossSources, _) = CrossSources.forInputs( inputs, preprocessors, TestLogger(), SuppressWarningOptions() ).orThrow val scopedSources = crossSources.scopedSources(BuildOptions()).orThrow val sources = scopedSources.sources( Scope.Main, crossSources.sharedOptions(BuildOptions()), root, TestLogger() ) .orThrow expect( sources.buildOptions.classPathOptions.extraDependencies.toSeq.map(_.value) == expectedDeps ) expect(sources.paths.isEmpty) expect(sources.inMemory.isEmpty) } } test("dependencies in .scala - using URL with query parameters") { val testInputs = TestInputs( os.rel / "something.scala" -> """| //> using file http://github.com/VirtusLab/scala-cli/blob/main/modules/dummy/amm/src/main/scala/AmmDummy.scala?version=3 | |object Main { |} |""".stripMargin ) testInputs.withInputs { (root, inputs) => val (crossSources, _) = CrossSources.forInputs( inputs, preprocessors, TestLogger(), SuppressWarningOptions(), download = _ => Right(Array.empty[Byte]) ).orThrow val scopedSources = crossSources.scopedSources(BuildOptions()).orThrow val sources = scopedSources.sources( Scope.Main, crossSources.sharedOptions(BuildOptions()), root, TestLogger() ).orThrow expect(sources.paths.length == 1) expect(sources.inMemory.length == 1) expect(sources.inMemory.head.generatedRelPath.last == "AmmDummy.scala") } } test("dependencies in .test.scala - using") { val testInputs = TestInputs( os.rel / "something.test.scala" -> """//> using deps org1:name1:1.1 org2::name2:2.2 |//> using dep org3:::name3:3.3 |import scala.collection.mutable | |object Something { | def a = 1 |} |""".stripMargin ) val expectedDeps = Nil testInputs.withInputs { (root, inputs) => val (crossSources, _) = CrossSources.forInputs( inputs, preprocessors, TestLogger(), SuppressWarningOptions() ).orThrow val scopedSources = crossSources.scopedSources(BuildOptions()).orThrow val sources = scopedSources.sources( Scope.Main, crossSources.sharedOptions(BuildOptions()), root, TestLogger() ) .orThrow expect( sources.buildOptions.classPathOptions.extraDependencies.toSeq.map(_.value) == expectedDeps ) expect(sources.paths.isEmpty) expect(sources.inMemory.isEmpty) } } test("dependencies in test/name.scala") { val files = Seq( os.rel / "test" / "something.scala" -> """//> using deps org1:name1:1.1 org2::name2:2.2 |//> using dep org3:::name3:3.3 |import scala.collection.mutable | |object Something { | def a = 1 |} |""".stripMargin ) val testInputs = TestInputs(files, Seq(".")) testInputs.withInputs { (root, inputs) => val (crossSources, _) = CrossSources.forInputs( inputs, preprocessors, TestLogger(), SuppressWarningOptions() ).orThrow val scopedSources = crossSources.scopedSources(BuildOptions()).orThrow val sources = scopedSources.sources( Scope.Main, crossSources.sharedOptions(BuildOptions()), root, TestLogger() ) .orThrow expect(sources.buildOptions.classPathOptions.extraDependencies.toSeq.map(_.value).isEmpty) expect(sources.paths.isEmpty) expect(sources.inMemory.isEmpty) } } test("dependencies in .scala - //> using") { val testInputs = TestInputs( os.rel / "something.scala" -> """//> using dep org1:name1:1.1 |//> using dep org2::name2:2.2 |//> using dep org3:::name3:3.3 |import scala.collection.mutable | |object Something { | def a = 1 |} |""".stripMargin ) val expectedDeps = Seq( dep"org1:name1:1.1", dep"org2::name2:2.2", dep"org3:::name3:3.3" ) testInputs.withInputs { (root, inputs) => val (crossSources, _) = CrossSources.forInputs( inputs, preprocessors, TestLogger(), SuppressWarningOptions() ).orThrow val scopedSources = crossSources.scopedSources(BuildOptions()).orThrow val sources = scopedSources.sources( Scope.Main, crossSources.sharedOptions(BuildOptions()), root, TestLogger() ) .orThrow expect( sources.buildOptions.classPathOptions.extraDependencies.toSeq.map(_.value) == expectedDeps ) expect(sources.paths.length == 1) val path = os.rel / "something.scala" expect(sources.paths.map(_._2) == Seq(path)) expect(sources.inMemory.isEmpty) } } test("dependencies in .java - //> using") { val testInputs = TestInputs( os.rel / "Something.java" -> """//> using dep org1:name1:1.1 |//> using dep org2::name2:2.2 |//> using dep org3:::name3:3.3 | |public class Something { | public Int a = 1; |} |""".stripMargin ) val expectedDeps = Seq( dep"org1:name1:1.1", dep"org2::name2:2.2", dep"org3:::name3:3.3" ) testInputs.withInputs { (root, inputs) => val (crossSources, _) = CrossSources.forInputs( inputs, preprocessors, TestLogger(), SuppressWarningOptions() ).orThrow val scopedSources = crossSources.scopedSources(BuildOptions()).orThrow val sources = scopedSources.sources( Scope.Main, crossSources.sharedOptions(BuildOptions()), root, TestLogger() ) .orThrow expect( sources.buildOptions.classPathOptions.extraDependencies.toSeq.map(_.value) == expectedDeps ) expect(sources.paths.length == 1) val path = os.rel / "Something.java" expect(sources.paths.map(_._2) == Seq(path)) expect(sources.inMemory.isEmpty) } } test("should skip SheBang in .sc and .scala") { val testInputs = TestInputs( os.rel / "something1.sc" -> """#!/usr/bin/env scala-cli | |println("Hello World")""".stripMargin, os.rel / "something2.sc" -> """#!/usr/bin/scala-cli | |println("Hello World")""".stripMargin, os.rel / "something3.sc" -> """#!/usr/bin/scala-cli |#! nix-shell -i scala-cli | |println("Hello World")""".stripMargin, os.rel / "something4.sc" -> """#!/usr/bin/scala-cli |#! nix-shell -i scala-cli | |!# | |println("Hello World")""".stripMargin, os.rel / "something5.sc" -> """#!/usr/bin/scala-cli | |println("Hello World #!")""".stripMargin, os.rel / "multiline.sc" -> """#!/usr/bin/scala-cli |# comment |VAL=1 |!# | |println("Hello World #!")""".stripMargin, os.rel / "hasBangHashInComment.sc" -> """#!/usr/bin/scala-cli | | | | |println("Hello World !#")""".stripMargin ) val expectedParsedCodes = Seq( """ |println("Hello World")""".stripMargin, """ |println("Hello World")""".stripMargin, """ | | |println("Hello World")""".stripMargin, """ | | | |println("Hello World")""".stripMargin, """ | |println("Hello World #!")""".stripMargin, """ | | | | |println("Hello World #!")""".stripMargin, """ | | | | |println("Hello World !#")""".stripMargin ) testInputs.withInputs { (root, inputs) => val (crossSources, _) = CrossSources.forInputs( inputs, preprocessors, TestLogger(), SuppressWarningOptions() ).orThrow val scopedSources = crossSources.scopedSources(BuildOptions()).orThrow val sources = scopedSources.sources( Scope.Main, crossSources.sharedOptions(BuildOptions()), root, TestLogger() ) .orThrow val parsedCodes: Seq[String] = sources.inMemory.map(_.content).map(s => new String(s, StandardCharsets.UTF_8)) parsedCodes.zip(expectedParsedCodes).foreach { case (parsedCode, expectedCode) => showDiff(parsedCode, expectedCode) expect(parsedCode.contains(expectedCode)) } } } def showDiff(parsed: String, expected: String): Unit = { if (!parsed.contains(expected)) for (((p, e), i) <- (parsed zip expected).zipWithIndex) { val ps = TestUtil.c2s(p) val es = TestUtil.c2s(e) if (ps != es) System.err.printf("%2d: [%s]!=[%s]\n", i, ps, es) } } test("dependencies in .sc - using") { val testInputs = TestInputs( os.rel / "something.sc" -> """//> using deps org1:name1:1.1 org2::name2:2.2 org3:::name3:3.3 |import scala.collection.mutable | |def a = 1 |""".stripMargin ) val expectedDeps = Seq( dep"org1:name1:1.1", dep"org2::name2:2.2", dep"org3:::name3:3.3" ) testInputs.withInputs { (root, inputs) => val (crossSources, _) = CrossSources.forInputs( inputs, preprocessors, TestLogger(), SuppressWarningOptions() ).orThrow val scopedSources = crossSources.scopedSources(BuildOptions()).orThrow val sources = scopedSources.sources( Scope.Main, crossSources.sharedOptions(BuildOptions()), root, TestLogger() ) .orThrow expect( sources.buildOptions.classPathOptions.extraDependencies.toSeq.map(_.value) == expectedDeps ) expect(sources.paths.isEmpty) expect(sources.inMemory.length == 1) val path = os.rel / "something.scala" expect(sources.inMemory.map(_.generatedRelPath) == Seq(path)) } } test("dependencies in .sc - //> using") { val testInputs = TestInputs( os.rel / "something.sc" -> """//> using dep org1:name1:1.1 |//> using dep org2::name2:2.2 |//> using dep org3:::name3:3.3 |import scala.collection.mutable | |def a = 1 |""".stripMargin ) val expectedDeps = Seq( dep"org1:name1:1.1", dep"org2::name2:2.2", dep"org3:::name3:3.3" ) testInputs.withInputs { (root, inputs) => val (crossSources, _) = CrossSources.forInputs( inputs, preprocessors, TestLogger(), SuppressWarningOptions() ).orThrow val scopedSources = crossSources.scopedSources(BuildOptions()).orThrow val sources = scopedSources.sources( Scope.Main, crossSources.sharedOptions(BuildOptions()), root, TestLogger() ) .orThrow expect( sources.buildOptions.classPathOptions.extraDependencies.toSeq.map(_.value) == expectedDeps ) expect(sources.paths.isEmpty) expect(sources.inMemory.length == 1) val path = os.rel / "something.scala" expect(sources.inMemory.map(_.generatedRelPath) == Seq(path)) } } test("java props in using directives") { val testInputs = TestInputs( os.rel / "something.sc" -> """//> using javaProp foo1 |//> using javaProp foo2=bar2 |""".stripMargin ) testInputs.withInputs { (root, inputs) => val (crossSources, _) = CrossSources.forInputs( inputs, preprocessors, TestLogger(), SuppressWarningOptions() ).orThrow val scopedSources = crossSources.scopedSources(BuildOptions()).orThrow val sources = scopedSources.sources( Scope.Main, crossSources.sharedOptions(BuildOptions()), root, TestLogger() ) .orThrow val javaOpts = sources.buildOptions.javaOptions.javaOpts.toSeq.sortBy(_.toString) val path = root / "something.sc" expect( javaOpts.head.value.value == "-Dfoo1", javaOpts.head.positions == Seq(Position.File(Right(path), (0, 19), (0, 23))), javaOpts(1).value.value == "-Dfoo2=bar2", javaOpts(1).positions == Seq(Position.File(Right(path), (1, 19), (1, 28))) ) } } test("java -XX:* options in using directives") { val (opt1, opt2, opt3) = ( "-XX:+UnlockExperimentalVMOptions", "-XX:+AlwaysPreTouch", "-XX:+UseParallelGC" ) val scriptPath = os.rel / "something.sc" val testInputs = TestInputs( scriptPath -> s"""//> using javaOpt $opt1 $opt2 $opt3 |""".stripMargin ) testInputs.withInputs { (root, inputs) => val (crossSources, _) = CrossSources.forInputs( inputs, preprocessors, TestLogger(), SuppressWarningOptions() ).orThrow val scopedSources = crossSources.scopedSources(BuildOptions()).orThrow val sources = scopedSources.sources( Scope.Main, crossSources.sharedOptions(BuildOptions()), root, TestLogger() ) .orThrow val javaOpts = sources.buildOptions.javaOptions.javaOpts.toSeq.sortBy(_.toString) val scriptAbsolutePath = root / scriptPath val startPosX = 0 val startPosY1 = 18 expect( javaOpts.head.value.value == opt1, javaOpts.head.positions == Seq(Position.File( Right(scriptAbsolutePath), (startPosX, startPosY1), (startPosX, startPosY1 + opt1.length) )) ) val startPosY2 = startPosY1 + opt1.length + 1 expect( javaOpts.drop(1).head.value.value == opt2, javaOpts.drop(1).head.positions == Seq(Position.File( Right(scriptAbsolutePath), (startPosX, startPosY2), (startPosX, startPosY2 + opt2.length) )) ) val startPosY3 = startPosY2 + opt2.length + 1 expect( javaOpts.drop(2).head.value.value == opt3, javaOpts.drop(2).head.positions == Seq(Position.File( Right(scriptAbsolutePath), (startPosX, startPosY3), (startPosX, startPosY3 + opt3.length) )) ) } } test("js options in using directives") { val testInputs = TestInputs( os.rel / "something.sc" -> """//> using jsVersion 1.8.0 |//> using jsMode mode |//> using jsNoOpt |//> using jsModuleKind commonjs |//> using jsCheckIr true |//> using jsEmitSourceMaps true |//> using jsDom true |//> using jsHeader "#!/usr/bin/env node\n" |//> using jsAllowBigIntsForLongs true |//> using jsAvoidClasses false |//> using jsAvoidLetsAndConsts false |//> using jsModuleSplitStyleStr smallestmodules |//> using jsEsVersionStr es2017 |""".stripMargin ) testInputs.withInputs { (root, inputs) => val (crossSources, _) = CrossSources.forInputs( inputs, preprocessors, TestLogger(), SuppressWarningOptions() ).orThrow val scopedSources = crossSources.scopedSources(BuildOptions()).orThrow val sources = scopedSources.sources( Scope.Main, crossSources.sharedOptions(BuildOptions()), root, TestLogger() ) .orThrow val jsOptions = sources.buildOptions.scalaJsOptions val jsConfig = jsOptions.linkerConfig(TestLogger()) expect( jsOptions.version.contains("1.8.0"), jsOptions.mode.nameOpt.contains("mode"), jsOptions.moduleKindStr.contains("commonjs"), jsOptions.checkIr.contains(true), jsOptions.emitSourceMaps, jsOptions.dom.contains(true), jsOptions.noOpt.contains(true) ) expect( jsConfig.moduleKind == ScalaJsLinkerConfig.ModuleKind.CommonJSModule, jsConfig.checkIR, jsConfig.sourceMap, jsConfig.jsHeader.contains("#!/usr/bin/env node\n"), jsConfig.esFeatures.allowBigIntsForLongs, !jsConfig.esFeatures.avoidClasses, !jsConfig.esFeatures.avoidLetsAndConsts, jsConfig.esFeatures.esVersion == "ES2017", jsConfig.moduleSplitStyle == ScalaJsLinkerConfig.ModuleSplitStyle.SmallestModules ) } } test("js options in using directives failure - multiple values") { val testInputs = TestInputs( os.rel / "something.sc" -> """//> using jsVersion 1.8.0 2.3.4 |""".stripMargin ) testInputs.withInputs { (_, inputs) => val crossSources = CrossSources.forInputs( inputs, preprocessors, TestLogger(), SuppressWarningOptions() ) crossSources match { case Left(_: UsingDirectiveValueNumError) => case o => fail("Exception expected", clues(o)) } } } test("js options in using directives failure - not a boolean") { val testInputs = TestInputs( os.rel / "something.sc" -> """//> using jsDom fasle |""".stripMargin ) testInputs.withInputs { (_, inputs) => val crossSources = CrossSources.forInputs( inputs, preprocessors, TestLogger(), SuppressWarningOptions() ) crossSources match { case Left(_: UsingDirectiveWrongValueTypeError) => case o => fail("Exception expected", clues(o)) } } } test("CrossSources.forInputs respects the order of inputs passed") { val inputArgs @ Seq(project, main, abc, message) = Seq("project.scala", "Main.scala", "Abc.scala", "Message.scala") val testInputs = TestInputs( os.rel / project -> """//> using dep com.lihaoyi::os-lib::0.8.1 |//> using file Message.scala |""".stripMargin, os.rel / main -> """object Main extends App { | println(Message(Abc.hello)) |} |""".stripMargin, os.rel / abc -> """object Abc { | val hello = "Hello" |} |""".stripMargin, os.rel / message -> """case class Message(value: String) |""".stripMargin ) testInputs.withInputs { (_, inputs) => val crossSourcesResult = CrossSources.forInputs( inputs, preprocessors, TestLogger(), SuppressWarningOptions() ) assert(crossSourcesResult.isRight) val CrossSources(onDiskSources, _, _, _, _, _) = crossSourcesResult.map(_._1) .getOrElse(sys.error("should not happen")) val onDiskPaths = onDiskSources.map(_.value._1.last) expect(onDiskPaths == inputArgs) } } } ================================================ FILE: modules/build/src/test/scala/scala/build/tests/TestInputs.scala ================================================ package scala.build.tests import bloop.rifle.BloopRifleConfig import java.nio.charset.StandardCharsets import scala.build.compiler.{BloopCompilerMaker, SimpleScalaCompilerMaker} import scala.build.errors.BuildException import scala.build.input.{Inputs, ScalaCliInvokeData} import scala.build.options.{BuildOptions, Scope} import scala.build.{Build, BuildThreads, Builds, Logger} import scala.util.Try import scala.util.control.NonFatal final case class TestInputs( files: Seq[(os.RelPath, String)], inputArgs: Seq[String] = Seq.empty, forceCwd: Option[os.Path] = None ) { def withInputs[T](f: (os.Path, Inputs) => T): T = withCustomInputs(false, None)(f) def fromRoot[T](f: os.Path => T, skipCreatingSources: Boolean = false): T = TestInputs.withTmpDir("scala-cli-tests-", forceCwd) { tmpDir => if skipCreatingSources then f(tmpDir) else { for ((relPath, content) <- files) { val path = tmpDir / relPath os.write(path, content.getBytes(StandardCharsets.UTF_8), createFolders = true) } f(tmpDir) } } def withCustomInputs[T]( viaDirectory: Boolean, forcedWorkspaceOpt: Option[os.FilePath], skipCreatingSources: Boolean = false )( f: (os.Path, Inputs) => T ): T = fromRoot( { tmpDir => val inputArgs0 = if (viaDirectory) Seq(tmpDir.toString) else if (inputArgs.isEmpty) files.map(_._1.toString) else inputArgs val res = Inputs( inputArgs0, tmpDir, forcedWorkspace = forcedWorkspaceOpt.map(_.resolveFrom(tmpDir)), allowRestrictedFeatures = true, extraClasspathWasPassed = false )(using ScalaCliInvokeData.dummy) res match { case Left(err) => throw new Exception(err) case Right(inputs) => f(tmpDir, inputs) } }, skipCreatingSources ) def withLoadedBuild[T]( options: BuildOptions, buildThreads: BuildThreads, // actually only used when bloopConfigOpt is non-empty bloopConfigOpt: Option[BloopRifleConfig], fromDirectory: Boolean = false )(f: (os.Path, Inputs, Build) => T): T = withBuild(options, buildThreads, bloopConfigOpt, fromDirectory)((p, i, maybeBuild) => maybeBuild match { case Left(e) => throw e case Right(b) => f(p, i, b) } ) def withLoadedBuilds[T]( options: BuildOptions, buildThreads: BuildThreads, // actually only used when bloopConfigOpt is non-empty bloopConfigOpt: Option[BloopRifleConfig], fromDirectory: Boolean = false )(f: (os.Path, Inputs, Builds) => T): T = withBuilds(options, buildThreads, bloopConfigOpt, fromDirectory)((p, i, builds) => builds match { case Left(e) => throw e case Right(b) => f(p, i, b) } ) def withBuilds[T]( options: BuildOptions, buildThreads: BuildThreads, // actually only used when bloopConfigOpt is non-empty bloopConfigOpt: Option[BloopRifleConfig], fromDirectory: Boolean = false, buildTests: Boolean = true, actionableDiagnostics: Boolean = false, skipCreatingSources: Boolean = false, logger: Option[Logger] = None )(f: (os.Path, Inputs, Either[BuildException, Builds]) => T): T = withCustomInputs(fromDirectory, None, skipCreatingSources) { (root, inputs) => val compilerMaker = bloopConfigOpt match { case Some(bloopConfig) => new BloopCompilerMaker( _ => Right(bloopConfig), buildThreads.bloop, strictBloopJsonCheck = true, offline = false ) case None => SimpleScalaCompilerMaker("java", Nil) } val log = logger.getOrElse(TestLogger()) val builds = Build.build( inputs, options, compilerMaker, None, log, crossBuilds = false, buildTests = buildTests, partial = None, actionableDiagnostics = Some(actionableDiagnostics) )(using ScalaCliInvokeData.dummy) f(root, inputs, builds) } def withBuild[T]( options: BuildOptions, buildThreads: BuildThreads, // actually only used when bloopConfigOpt is non-empty bloopConfigOpt: Option[BloopRifleConfig], fromDirectory: Boolean = false, buildTests: Boolean = true, actionableDiagnostics: Boolean = false, scope: Scope = Scope.Main, skipCreatingSources: Boolean = false, logger: Option[Logger] = None )(f: (os.Path, Inputs, Either[BuildException, Build]) => T): T = withBuilds( options, buildThreads, bloopConfigOpt, fromDirectory, buildTests = buildTests, actionableDiagnostics = actionableDiagnostics, skipCreatingSources = skipCreatingSources, logger = logger ) { (p, i, builds) => f( p, i, builds.map(_.get(scope).getOrElse(sys.error(s"No ${scope.name} build found"))) ) } } object TestInputs { def apply(files: (os.RelPath, String)*): TestInputs = TestInputs(files, Nil) def withTmpDir[T](prefix: String, forceCwd: Option[os.Path] = None)(f: os.Path => T): T = forceCwd match { case Some(path) => f(path) case None => val tmpDir = os.temp.dir(prefix = prefix) try f(tmpDir) finally tryRemoveAll(tmpDir) } def tryRemoveAll(f: os.Path): Unit = try os.remove.all(f) catch { case ex: java.nio.file.FileSystemException => System.err.println(s"Could not remove $f ($ex), will try to remove it upon JVM shutdown.") System.err.println(s"find $f = '${Try(os.walk(f))}'") Runtime.getRuntime.addShutdownHook( new Thread("remove-dir-windows") { setDaemon(true) override def run(): Unit = try os.remove.all(f) catch { case NonFatal(e) => System.err.println(s"Caught $e while trying to remove $f, ignoring it.") } } ) } } ================================================ FILE: modules/build/src/test/scala/scala/build/tests/TestLogger.scala ================================================ package scala.build.tests import bloop.rifle.BloopRifleLogger import coursier.cache.CacheLogger import coursier.cache.loggers.{FallbackRefreshDisplay, RefreshLogger} import org.scalajs.logging.{Logger as ScalaJsLogger, NullLogger} import java.io.PrintStream import scala.build.Logger import scala.build.errors.{BuildException, Diagnostic} import scala.build.internals.FeatureType import scala.collection.mutable.ListBuffer import scala.scalanative.build as sn /** Logger that records all message() and log() calls for test assertions. */ final class RecordingLogger(delegate: Logger = TestLogger()) extends Logger { val messages: ListBuffer[String] = ListBuffer.empty override def error(message: String): Unit = delegate.error(message) override def message(message: => String): Unit = { val msg = message messages += msg delegate.message(msg) } override def log(s: => String): Unit = { val msg = s messages += msg delegate.log(msg) } override def log(s: => String, debug: => String): Unit = delegate.log(s, debug) override def debug(s: => String): Unit = delegate.debug(s) override def log(diagnostics: Seq[Diagnostic]): Unit = delegate.log(diagnostics) override def log(ex: BuildException): Unit = delegate.log(ex) override def debug(ex: BuildException): Unit = delegate.debug(ex) override def exit(ex: BuildException): Nothing = delegate.exit(ex) override def coursierLogger(message: String): CacheLogger = delegate.coursierLogger(message) override def bloopRifleLogger: BloopRifleLogger = delegate.bloopRifleLogger override def scalaJsLogger: ScalaJsLogger = delegate.scalaJsLogger override def scalaNativeTestLogger: sn.Logger = delegate.scalaNativeTestLogger override def scalaNativeCliInternalLoggerOptions: List[String] = delegate.scalaNativeCliInternalLoggerOptions override def compilerOutputStream: PrintStream = delegate.compilerOutputStream override def verbosity: Int = delegate.verbosity override def experimentalWarning(featureName: String, featureType: FeatureType): Unit = delegate.experimentalWarning(featureName, featureType) override def flushExperimentalWarnings: Unit = delegate.flushExperimentalWarnings } case class TestLogger(info: Boolean = true, debug: Boolean = false) extends Logger { override def log(diagnostics: Seq[Diagnostic]): Unit = { diagnostics.foreach { d => System.err.println(d.positions.map(_.render()).mkString("/") ++ ": " ++ d.message) } } def error(message: String): Unit = System.err.println(message) def message(message: => String): Unit = if (info) System.err.println(message) def log(s: => String): Unit = if (info) System.err.println(s) def log(s: => String, debug: => String): Unit = if (this.debug) System.err.println(debug) else if (info) System.err.println(s) def debug(s: => String): Unit = if (debug) System.err.println(s) def log(ex: BuildException): Unit = System.err.println(ex.getMessage) def debug(ex: BuildException): Unit = debug(ex.getMessage) def exit(ex: BuildException): Nothing = throw new Exception(ex) def coursierLogger(message: String): CacheLogger = RefreshLogger.create(new FallbackRefreshDisplay) def bloopRifleLogger: BloopRifleLogger = if (debug) new BloopRifleLogger { def bloopBspStderr: Option[java.io.OutputStream] = Some(System.err) def bloopBspStdout: Option[java.io.OutputStream] = Some(System.out) def bloopCliInheritStderr: Boolean = true def bloopCliInheritStdout: Boolean = true def debug(msg: => String): Unit = { System.err.println(msg) } def debug(msg: => String, ex: Throwable): Unit = { System.err.println(msg) if (ex != null) ex.printStackTrace(System.err) } def error(msg: => String, ex: Throwable): Unit = { System.err.println(msg) if (ex != null) ex.printStackTrace(System.err) } def error(msg: => String): Unit = System.err.println(msg) def info(msg: => String): Unit = System.err.println(msg) } else BloopRifleLogger.nop def scalaJsLogger: ScalaJsLogger = NullLogger def scalaNativeTestLogger: sn.Logger = sn.Logger.nullLogger def scalaNativeCliInternalLoggerOptions: List[String] = List() def compilerOutputStream: PrintStream = System.err def verbosity: Int = if debug then 2 else if info then 0 else -1 override def experimentalWarning(featureName: String, featureType: FeatureType): Unit = System.err.println(s"Experimental $featureType `$featureName` used") override def flushExperimentalWarnings: Unit = () } ================================================ FILE: modules/build/src/test/scala/scala/build/tests/TestUtil.scala ================================================ package scala.build.tests import munit.AnyFixture import munit.Assertions.assertEquals import java.util.concurrent.TimeUnit import scala.build.options.{BuildOptions, Platform} import scala.build.{Build, Positioned} import scala.concurrent.duration.FiniteDuration object TestUtil { abstract class ScalaCliBuildSuite extends munit.FunSuite { extension (munitContext: BeforeEach | AfterEach) { def locationAbsolutePath: os.Path = os.Path { (munitContext match { case beforeEach: BeforeEach => beforeEach.test case afterEach: AfterEach => afterEach.test }).location.path } } override def munitTimeout = new FiniteDuration(120, TimeUnit.SECONDS) val testStartEndLogger: Fixture[Unit] = new Fixture[Unit]("files") { def apply(): Unit = () override def beforeEach(context: BeforeEach): Unit = { val fileName = context.locationAbsolutePath.baseName System.err.println( s">==== ${Console.CYAN}Running '${context.test.name}' from $fileName${Console.RESET}" ) } override def afterEach(context: AfterEach): Unit = { val fileName = context.locationAbsolutePath.baseName System.err.println( s"X==== ${Console.CYAN}Finishing '${context.test.name}' from $fileName${Console.RESET}" ) } } override def munitFixtures: Seq[AnyFixture[?]] = List(testStartEndLogger) } val isCI: Boolean = System.getenv("CI") != null implicit class TestBuildOps(private val build: Build) extends AnyVal { private def successfulBuild: Build.Successful = build.successfulOpt.getOrElse { sys.error("Compilation failed") } def generated(): Seq[os.RelPath] = os.walk(successfulBuild.output) .filter(os.isFile(_)) .map(_.relativeTo(successfulBuild.output)) def assertGeneratedEquals(expected: String*): Unit = { val generated0 = generated() assert( generated0.map(_.toString).toSet == expected.toSet, { pprint.log(generated0.map(_.toString).sorted) pprint.log(expected.sorted) "" } ) } def assertNoDiagnostics(): Unit = assertEquals(build.diagnostics.toSeq.flatten, Nil) } implicit class TestBuildOptionsOps(private val options: BuildOptions) extends AnyVal { def enableJs: BuildOptions = options.copy( scalaOptions = options.scalaOptions.copy( platform = Some(Positioned.none(Platform.JS)) ) ) def enableNative: BuildOptions = options.copy( scalaOptions = options.scalaOptions.copy( platform = Some(Positioned.none(Platform.Native)) ) ) } implicit class TestAnyOps[T](private val x: T) extends AnyVal { def is(expected: T): Unit = assert( x == expected, { pprint.log(x) pprint.log(expected) "Assertion failed" } ) } def c2s(c: Char): String = c match { case '\r' => "\\r" case '\n' => "\\n" case s => s"$s" } lazy val cs: String = Constants.cs } ================================================ FILE: modules/build/src/test/scala/scala/build/tests/markdown/MarkdownCodeBlockTests.scala ================================================ package scala.build.tests.markdown import com.eed3si9n.expecty.Expecty.expect import scala.build.Position import scala.build.errors.{BuildException, MarkdownUnclosedBackticksError} import scala.build.internal.markdown.MarkdownCodeBlock import scala.build.tests.TestUtil import scala.build.tests.markdown.MarkdownTestUtil.* class MarkdownCodeBlockTests extends TestUtil.ScalaCliBuildSuite { test("no code blocks are extracted from markdown if none are present") { val markdown: String = """ |# Heading |Lorem ipsum dolor sit amet, |consectetur adipiscing elit, | |## Subheading |sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. |Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. |Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. |Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. |""".stripMargin val path = os.sub / "Example.md" expect(MarkdownCodeBlock.findCodeBlocks(path, markdown) == Right(Seq.empty)) } test("a simple Scala code block is extracted correctly from markdown") { val code = """println("Hello")""" val markdown = s"""# Some snippet | |```scala |$code |``` |""".stripMargin val expectedResult = MarkdownCodeBlock( info = PlainScalaInfo, body = code, startLine = 3, endLine = 3 ) val path = os.sub / "Example.md" val actualResult = MarkdownCodeBlock.findCodeBlocks(path, markdown) .getOrElse(sys.error("failed while finding code blocks")) .head expect(actualResult == expectedResult) } test("shebang line is ignored in plain scala code blocks") { val code = """println("Hello")""".stripMargin val markdown = s"""# Some snippet | |```scala |#!/usr/bin/env -S scala-cli shebang |$code |``` |""".stripMargin val expectedResult = MarkdownCodeBlock( info = PlainScalaInfo, body = "\n" + code, startLine = 3, endLine = 4 ) val path = os.sub / "Example.md" val actualResult: MarkdownCodeBlock = MarkdownCodeBlock.findCodeBlocks(path, markdown) .getOrElse(sys.error("failed while finding code blocks")) .head showDiffs(actualResult, expectedResult) expect(actualResult == expectedResult) } test("end-of-shebang token allowed in scala code") { val code = """println("Hello !#")""".stripMargin val markdown = s"""# Some snippet | |```scala |#!/usr/bin/env -S scala-cli shebang |$code |``` |""".stripMargin val expectedResult = MarkdownCodeBlock( info = PlainScalaInfo, body = "\n" + code, startLine = 3, endLine = 4 ) val path = os.sub / "Example.md" val actualResult: MarkdownCodeBlock = MarkdownCodeBlock.findCodeBlocks(path, markdown) .getOrElse(sys.error("failed while finding code blocks")) .head showDiffs(actualResult, expectedResult) expect(actualResult == expectedResult) } test("a raw Scala code block is extracted correctly from markdown") { val code = """object Main extends App { | println("Hello") |}""".stripMargin val markdown = s"""# Some snippet | |```scala raw |$code |``` |""".stripMargin val expectedResult = MarkdownCodeBlock( info = RawScalaInfo, body = code, startLine = 3, endLine = 5 ) val path = os.sub / "Example.md" val actualResult = MarkdownCodeBlock.findCodeBlocks(path, markdown) .getOrElse(sys.error("failed while finding code blocks")) .head expect(actualResult == expectedResult) } test("shebang line is ignored in raw scala code blocks") { val code = """object Main extends App { | println("Hello") |}""".stripMargin val markdown = s"""# Some snippet | |```scala raw |#!/usr/bin/env -S scala-cli shebang |$code |``` |""".stripMargin val expectedResult = MarkdownCodeBlock( info = RawScalaInfo, body = "\n" + code, startLine = 3, endLine = 6 ) val path = os.sub / "Example.md" val actualResult: MarkdownCodeBlock = MarkdownCodeBlock.findCodeBlocks(path, markdown) .getOrElse(sys.error("failed while finding code blocks")) .head showDiffs(actualResult, expectedResult) expect(actualResult == expectedResult) } test("a test Scala snippet is extracted correctly from markdown") { val code = """//> using dep org.scalameta::munit:0.7.29 |class Test extends munit.FunSuite { | assert(true) |}""".stripMargin val markdown = s"""# Some snippet | |```scala test |$code |``` |""".stripMargin val expectedResult = MarkdownCodeBlock( info = TestScalaInfo, body = code, startLine = 3, endLine = 6 ) val path = os.sub / "Example.md" val actualResult = MarkdownCodeBlock.findCodeBlocks(path, markdown) .getOrElse(sys.error("failed while finding code blocks")) .head expect(actualResult == expectedResult) } test("a Scala code block is skipped when it's tagged as `ignore` in markdown") { val code = """println("Hello")""" val markdown = s"""# Some snippet | |```scala ignore |$code |``` |""".stripMargin val path = os.sub / "Example.md" expect(MarkdownCodeBlock.findCodeBlocks(path, markdown) == Right(Seq.empty)) } test("an unclosed code block produces a build error") { val code = """println("Hello")""" val markdown = s"""# Some snippet | |```scala |$code |""".stripMargin val subPath = os.sub / "Example.md" val path = os.pwd / subPath val expectedPosition = Position.File(Right(path), 2 -> 0, 2 -> 3) val expectedError = MarkdownUnclosedBackticksError("```", Seq(expectedPosition)) val actualException = MarkdownCodeBlock.findCodeBlocks(subPath, markdown) .left.getOrElse(sys.error("failed while finding code blocks")) expect(actualException.message == expectedError.message) expect(actualException.positions == expectedError.positions) } test("recovery from an unclosed code block error works correctly") { val code = """println("closed snippet")""" val markdown = """# Some snippet |```scala |println("closed snippet") |``` | |# Some other snippet | |````scala |println("unclosed snippet") | |```scala |println("whatever") |``` |""".stripMargin val subPath = os.sub / "Example.md" var maybeError: Option[BuildException] = None val recoveryFunction = (be: BuildException) => { maybeError = Some(be) None } val actualResult = MarkdownCodeBlock.findCodeBlocks(subPath, markdown, maybeRecoverOnError = recoveryFunction) .getOrElse(sys.error("failed while finding code blocks")) .head val expectedResult = MarkdownCodeBlock( info = PlainScalaInfo, body = code, startLine = 2, endLine = 2 ) expect(actualResult == expectedResult) val path = os.pwd / subPath val expectedPosition = Position.File(Right(path), 7 -> 0, 7 -> 4) val expectedError = MarkdownUnclosedBackticksError("````", Seq(expectedPosition)) val actualError = maybeError.get expect(actualError.positions == expectedError.positions) expect(actualError.message == expectedError.message) } def showDiffs(actual: MarkdownCodeBlock, expect: MarkdownCodeBlock): Unit = { if actual != expect then for (((a, b), i) <- (actual.body zip expect.body).zipWithIndex) if (a != b) { val aa = TestUtil.c2s(a) val bb = TestUtil.c2s(b) System.err.printf("== index %d: [%s]!=[%s]\n", i, aa, bb) } System.err.printf("actual[%s]\nexpect[%s]\n", actual, expect) } } ================================================ FILE: modules/build/src/test/scala/scala/build/tests/markdown/MarkdownCodeWrapperTests.scala ================================================ package scala.build.tests.markdown import com.eed3si9n.expecty.Expecty.expect import scala.build.internal.AmmUtil import scala.build.internal.markdown.{MarkdownCodeBlock, MarkdownCodeWrapper} import scala.build.preprocessing.{PreprocessedMarkdown, PreprocessedMarkdownCodeBlocks} import scala.build.tests.TestUtil import scala.build.tests.markdown.MarkdownTestUtil.* import scala.language.reflectiveCalls class MarkdownCodeWrapperTests extends TestUtil.ScalaCliBuildSuite { test("empty markdown produces no wrapped code") { val result = MarkdownCodeWrapper(os.sub / "Example.md", PreprocessedMarkdown.empty) expect(result == (None, None, None)) } test("a simple Scala code block is wrapped correctly") { val snippet = """println("Hello")""" val codeBlock = MarkdownCodeBlock(PlainScalaInfo, snippet, 3, 3) val preprocessedCodeBlocks = PreprocessedMarkdownCodeBlocks(Seq(codeBlock)) val markdown = PreprocessedMarkdown(scriptCodeBlocks = preprocessedCodeBlocks) val expectedScala = MarkdownCodeWrapper.WrappedMarkdownCode( s"""object Example_md { @annotation.nowarn("msg=pure expression does nothing") def main(args: Array[String]): Unit = { Scope; } | |object Scope { |$snippet |}}""".stripMargin ) val result = MarkdownCodeWrapper(os.sub / "Example.md", markdown) showDiffs(result, expectedScala.code) expect(result == (Some(expectedScala), None, None)) } test("multiple plain Scala code blocks are wrapped correctly") { val snippet1 = """println("Hello")""" val codeBlock1 = MarkdownCodeBlock(PlainScalaInfo, snippet1, 3, 3) val snippet2 = """println("world")""" val codeBlock2 = MarkdownCodeBlock(PlainScalaInfo, snippet2, 8, 8) val snippet3 = """println("!")""" val codeBlock3 = MarkdownCodeBlock(PlainScalaInfo, snippet3, 12, 12) val preprocessedCodeBlocks = PreprocessedMarkdownCodeBlocks(Seq(codeBlock1, codeBlock2, codeBlock3)) val markdown = PreprocessedMarkdown(scriptCodeBlocks = preprocessedCodeBlocks) val expectedScala = MarkdownCodeWrapper.WrappedMarkdownCode( s"""object Example_md { @annotation.nowarn("msg=pure expression does nothing") def main(args: Array[String]): Unit = { Scope; } | |object Scope { |$snippet1 | | | | |$snippet2 | | | |$snippet3 |}}""".stripMargin ) val result = MarkdownCodeWrapper(os.sub / "Example.md", markdown) showDiffs(result, expectedScala.code) expect(result == (Some(expectedScala), None, None)) } test("multiple plain Scala code blocks with different scopes are wrapped correctly") { val snippet1 = """println("Hello")""" val codeBlock1 = MarkdownCodeBlock(PlainScalaInfo, snippet1, 3, 3) val snippet2 = """println("world")""" val codeBlock2 = MarkdownCodeBlock(ResetScalaInfo, snippet2, 8, 8) val snippet3 = """println("!")""" val codeBlock3 = MarkdownCodeBlock(PlainScalaInfo, snippet3, 12, 12) val preprocessedCodeBlocks = PreprocessedMarkdownCodeBlocks(Seq(codeBlock1, codeBlock2, codeBlock3)) val markdown = PreprocessedMarkdown(scriptCodeBlocks = preprocessedCodeBlocks) val expectedScala = MarkdownCodeWrapper.WrappedMarkdownCode( s"""object Example_md { @annotation.nowarn("msg=pure expression does nothing") def main(args: Array[String]): Unit = { Scope; Scope1; } | |object Scope { |$snippet1 | | | |}; object Scope1 { |$snippet2 | | | |$snippet3 |}}""".stripMargin ) val result = MarkdownCodeWrapper(os.sub / "Example.md", markdown) showDiffs(result, expectedScala.code) expect(result == (Some(expectedScala), None, None)) } test("a raw Scala code block is wrapped correctly") { val snippet = """object Main extends App { | println("Hello") |}""".stripMargin val codeBlock = MarkdownCodeBlock(RawScalaInfo, snippet, 3, 5) val preprocessedCodeBlocks = PreprocessedMarkdownCodeBlocks(Seq(codeBlock)) val markdown = PreprocessedMarkdown(rawCodeBlocks = preprocessedCodeBlocks) val expectedScala = MarkdownCodeWrapper.WrappedMarkdownCode( s""" | | |$snippet |""".stripMargin ) val result = MarkdownCodeWrapper(os.sub / "Example.md", markdown) showDiffs(result, expectedScala.code) expect(result == (None, Some(expectedScala), None)) } test("multiple raw Scala code blocks are glued together correctly") { val snippet1 = """case class Message(value: String)""".stripMargin val codeBlock1 = MarkdownCodeBlock(RawScalaInfo, snippet1, 3, 3) val snippet2 = """object Main extends App { | println(Message("Hello").value) |}""".stripMargin val codeBlock2 = MarkdownCodeBlock(RawScalaInfo, snippet2, 5, 7) val preprocessedCodeBlocks = PreprocessedMarkdownCodeBlocks(Seq(codeBlock1, codeBlock2)) val markdown = PreprocessedMarkdown(rawCodeBlocks = preprocessedCodeBlocks) val expectedScala = MarkdownCodeWrapper.WrappedMarkdownCode( s""" | | |$snippet1 | |$snippet2 |""".stripMargin ) val result = MarkdownCodeWrapper(os.sub / "Example.md", markdown) showDiffs(result, expectedScala.code) expect(result == (None, Some(expectedScala), None)) } test("a test Scala snippet is wrapped correctly") { val snippet = """//> using dep org.scalameta::munit:0.7.29 |class Test extends munit.FunSuite { | assert(true) |}""".stripMargin val codeBlock = MarkdownCodeBlock(TestScalaInfo, snippet, 3, 6) val preprocessedCodeBlocks = PreprocessedMarkdownCodeBlocks(Seq(codeBlock)) val markdown = PreprocessedMarkdown(testCodeBlocks = preprocessedCodeBlocks) val expectedScala = MarkdownCodeWrapper.WrappedMarkdownCode( s""" | | |$snippet |""".stripMargin ) val result = MarkdownCodeWrapper(os.sub / "Example.md", markdown) showDiffs(result, expectedScala.code) expect(result == (None, None, Some(expectedScala))) } def stringPrep(s: String): String = AmmUtil.normalizeNewlines(s) test("multiple test Scala snippets are glued together correctly") { val snippet1 = stringPrep( """//> using dep org.scalameta::munit:0.7.29 |class Test1 extends munit.FunSuite { | assert(true) |}""".stripMargin ) val codeBlock1 = MarkdownCodeBlock(TestScalaInfo, snippet1, 3, 6) val snippet2 = stringPrep( """class Test2 extends munit.FunSuite { | assert(true) |}""".stripMargin ) val codeBlock2 = MarkdownCodeBlock(TestScalaInfo, snippet2, 8, 10) val preprocessedCodeBlocks = PreprocessedMarkdownCodeBlocks(Seq(codeBlock1, codeBlock2)) val markdown = PreprocessedMarkdown(testCodeBlocks = preprocessedCodeBlocks) val expectedScala = MarkdownCodeWrapper.WrappedMarkdownCode( stringPrep( s""" | | |$snippet1 | |$snippet2 |""".stripMargin ) ) val result = MarkdownCodeWrapper(os.sub / "Example.md", markdown) showDiffs(result, expectedScala.code) expect(result == (None, None, Some(expectedScala))) } import scala.reflect.Selectable.reflectiveSelectable type HasCode = { def code: String } type CodeWrapper = (Option[HasCode], Option[HasCode], Option[HasCode]) def showDiffs(result: CodeWrapper, expect: String): Unit = { val actual: String = result match { case (Some(s), None, None) => s.code case (None, Some(s), None) => s.code case (None, None, Some(s)) => s.code case _ => result.toString } if actual != expect then for (((a, b), i) <- (actual zip expect).zipWithIndex) if (a != b) { val aa = TestUtil.c2s(a) val bb = TestUtil.c2s(b) System.err.printf("== index %d: [%s]!=[%s]\n", i, aa, bb) } System.err.printf("actual[%s]\nexpect[%s]\n", actual, expect) } } ================================================ FILE: modules/build/src/test/scala/scala/build/tests/markdown/MarkdownTestUtil.scala ================================================ package scala.build.tests.markdown object MarkdownTestUtil { val PlainScalaInfo: Seq[String] = Seq("scala") val RawScalaInfo: Seq[String] = Seq("scala", "raw") val TestScalaInfo: Seq[String] = Seq("scala", "test") val ResetScalaInfo: Seq[String] = Seq("scala", "reset") } ================================================ FILE: modules/build/src/test/scala/scala/build/tests/util/BloopServer.scala ================================================ package scala.build.tests.util import bloop.rifle.BloopRifleConfig import coursier.cache.FileCache import scala.build.internals.EnvVar import scala.build.{Bloop, Logger} import scala.util.Properties object BloopServer { private def directories = scala.build.Directories.default() // FIXME We could use a test-specific Bloop instance here. // Not sure how to properly shut it down or have it exit after a period // of inactivity, so we keep using our default global Bloop for now. private def bloopAddress = BloopRifleConfig.Address.DomainSocket(directories.bloopDaemonDir.toNIO) val bloopConfig = { val base = BloopRifleConfig.default( bloopAddress, v => Bloop.bloopClassPath(Logger.nop, FileCache(), v), directories.bloopWorkingDir.toIO ) base.copy( javaPath = if (Properties.isWin) base.javaPath else // On Linux / macOS, we start the Bloop server via /bin/sh, // which can have issues with the directory of "java" in the PATH, // if it contains '+' or '%' IIRC. // So we hardcode the path to "java" here. EnvVar.Java.javaHome.valueOpt .map(os.Path(_, os.pwd)) .map(_ / "bin" / "java") .map(_.toString) .getOrElse(base.javaPath) ) } } ================================================ FILE: modules/build-macros/src/main/scala/scala/build/EitherCps.scala ================================================ package scala.build final case class EitherFailure[E](v: E, cps: EitherCps[?]) extends RuntimeException: override def fillInStackTrace() = this // disable stack trace generation class EitherCps[E] object EitherCps: def value[E, V](using cps: EitherCps[? >: E] )(from: Either[E, V]) = // Adding a context bounds breaks incremental compilation from match case Left(e) => throw EitherFailure(e, cps) case Right(v) => v final class Helper[E](): def apply[V](op: EitherCps[E] ?=> V): Either[E, V] = val cps = new EitherCps[E] try Right(op(using cps)) catch case EitherFailure(e: E @unchecked, `cps`) => Left(e) def either[E]: Helper[E] = new Helper[E]() ================================================ FILE: modules/build-macros/src/main/scala/scala/build/EitherSequence.scala ================================================ package scala.build import scala.collection.mutable.ListBuffer object EitherSequence { def sequence[E, T](eithers: Seq[Either[E, T]]): Either[::[E], Seq[T]] = { val errors = new ListBuffer[E] val values = new ListBuffer[T] eithers.foreach { case Left(e) => errors += e case Right(t) => if (errors.isEmpty) values += t } errors.result() match { case Nil => Right(values.result()) case h :: t => Left(::(h, t)) } } } ================================================ FILE: modules/build-macros/src/main/scala/scala/build/Ops.scala ================================================ package scala.build import scala.collection.mutable.ListBuffer object Ops { implicit class EitherSeqOps[E, T](private val seq: Seq[Either[E, T]]) extends AnyVal { def sequence: Either[::[E], Seq[T]] = EitherSequence.sequence(seq) } implicit class EitherIteratorOps[E, T](private val it: Iterator[Either[E, T]]) extends AnyVal { def sequence0: Either[E, Seq[T]] = { val b = new ListBuffer[T] var errOpt = Option.empty[E] while (it.hasNext && errOpt.isEmpty) { val e = it.next() e match { case Left(err) => errOpt = Some(err) case Right(t) => b += t } } errOpt.toLeft(b.result()) } } implicit class EitherOptOps[E, T](private val opt: Option[Either[E, T]]) extends AnyVal { def sequence: Either[E, Option[T]] = opt match { case None => Right(None) case Some(Left(e)) => Left(e) case Some(Right(t)) => Right(Some(t)) } } implicit class EitherThrowOps[E <: Throwable, T](private val either: Either[E, T]) extends AnyVal { def orThrow: T = either match { case Left(e) => throw new Exception(e) case Right(t) => t } } implicit class EitherMap2[Ex <: Throwable, ExA <: Ex, ExB <: Ex, A, B]( private val eithers: (Either[ExA, A], Either[ExB, B]) ) extends AnyVal { def traverseN: Either[::[Ex], (A, B)] = eithers match { case (Right(a), Right(b)) => Right((a, b)) case _ => val errors = eithers._1.left.toOption.toSeq ++ eithers._2.left.toOption.toSeq val errors0 = errors.toList match { case Nil => sys.error("Cannot happen") case h :: t => ::(h, t) } Left(errors0) } } implicit class EitherMap3[Ex <: Throwable, ExA <: Ex, ExB <: Ex, ExC <: Ex, A, B, C]( private val eithers: (Either[ExA, A], Either[ExB, B], Either[ExC, C]) ) extends AnyVal { def traverseN: Either[::[Ex], (A, B, C)] = eithers match { case (Right(a), Right(b), Right(c)) => Right((a, b, c)) case _ => val errors = eithers._1.left.toOption.toSeq ++ eithers._2.left.toOption.toSeq ++ eithers._3.left.toOption.toSeq val errors0 = errors.toList match { case Nil => sys.error("Cannot happen") case h :: t => ::(h, t) } Left(errors0) } } implicit class EitherMap4[ Ex <: Throwable, ExA <: Ex, ExB <: Ex, ExC <: Ex, ExD <: Ex, A, B, C, D ]( private val eithers: (Either[ExA, A], Either[ExB, B], Either[ExC, C], Either[ExD, D]) ) extends AnyVal { def traverseN: Either[::[Ex], (A, B, C, D)] = eithers match { case (Right(a), Right(b), Right(c), Right(d)) => Right((a, b, c, d)) case _ => val errors = eithers._1.left.toOption.toSeq ++ eithers._2.left.toOption.toSeq ++ eithers._3.left.toOption.toSeq ++ eithers._4.left.toOption.toSeq val errors0 = errors.toList match { case Nil => sys.error("Cannot happen") case h :: t => ::(h, t) } Left(errors0) } } implicit class EitherMap5[ Ex <: Throwable, ExA <: Ex, ExB <: Ex, ExC <: Ex, ExD <: Ex, ExE <: Ex, A, B, C, D, E ]( private val eithers: ( Either[ExA, A], Either[ExB, B], Either[ExC, C], Either[ExD, D], Either[ExE, E] ) ) extends AnyVal { def traverseN: Either[::[Ex], (A, B, C, D, E)] = eithers match { case (Right(a), Right(b), Right(c), Right(d), Right(e)) => Right((a, b, c, d, e)) case _ => val errors = eithers._1.left.toOption.toSeq ++ eithers._2.left.toOption.toSeq ++ eithers._3.left.toOption.toSeq ++ eithers._4.left.toOption.toSeq ++ eithers._5.left.toOption.toSeq val errors0 = errors.toList match { case Nil => sys.error("Cannot happen") case h :: t => ::(h, t) } Left(errors0) } } implicit class EitherMap6[ Ex <: Throwable, ExA <: Ex, ExB <: Ex, ExC <: Ex, ExD <: Ex, ExE <: Ex, ExF <: Ex, A, B, C, D, E, F ]( private val eithers: ( Either[ExA, A], Either[ExB, B], Either[ExC, C], Either[ExD, D], Either[ExE, E], Either[ExF, F] ) ) extends AnyVal { def traverseN: Either[::[Ex], (A, B, C, D, E, F)] = eithers match { case (Right(a), Right(b), Right(c), Right(d), Right(e), Right(f)) => Right((a, b, c, d, e, f)) case _ => val errors = eithers._1.left.toOption.toSeq ++ eithers._2.left.toOption.toSeq ++ eithers._3.left.toOption.toSeq ++ eithers._4.left.toOption.toSeq ++ eithers._5.left.toOption.toSeq ++ eithers._6.left.toOption.toSeq val errors0 = errors.toList match { case Nil => sys.error("Cannot happen") case h :: t => ::(h, t) } Left(errors0) } } } ================================================ FILE: modules/build-macros/src/negative-tests/MismatchedLeft.scala ================================================ import scala.build.EitherCps.* class E class EE1 extends E class EE2 extends E class E2 class V class VV extends V val vv: Either[E, VV] = Right(new VV) def ee3: Either[E2, V] = either { value(Left(new EE1)) value(vv.left.map(_ => new EE2)) new V } ================================================ FILE: modules/build-macros/src/test/scala/scala/build/CPSTest.scala ================================================ package scala.build import scala.build.EitherCps.* class CPSTest extends munit.FunSuite { val failed1: Either[Int, String] = Left(1) val ok: Either[Int, String] = Right("OK") def checkResult(expected: Either[Int, String])(res: => Either[Int, String]): Unit = assertEquals(expected, res) test("Basic CPS test") { checkResult(Right("OK"))(either("OK")) checkResult(Right("OK"))(either(value(ok))) checkResult(Left(1))(either(value(failed1))) } test("Exceptions") { intercept[IllegalArgumentException](either(throw new IllegalArgumentException("test"))) intercept[IllegalArgumentException](either { throw new IllegalArgumentException("test") value(failed1) }) } test("early return") { checkResult(Left(1)) { either { value(failed1) throw new IllegalArgumentException("test") } } } class E class EE extends E class EEE extends E class V class VV extends V class E2 { def ala = 123 } class V2 val ee: Either[EE, V] = Left(new EE) val vv: Either[E, VV] = Right(new VV) def ee2: Either[E, V] = either { value(Left(new EEE)) value(vv.left.map(_ => new EE)) new V } // Mainly to see if compiles test("variance 123") { val errorRes: Either[E, V] = either(value(ee)) assert(ee == errorRes) val valueRes: Either[E, V] = either(value(vv)) assert(vv == valueRes) } test("fallback to Right") { val res = new V2 val a: Either[E2, V2] = either { value(Right(res)) } assert(Right(res) == a) } } ================================================ FILE: modules/cli/src/main/java/scala/cli/commands/pgp/PgpCommandsSubst.java ================================================ package scala.cli.commands.pgp; import com.oracle.svm.core.annotate.Substitute; import com.oracle.svm.core.annotate.TargetClass; import scala.cli.commands.pgp.ExternalCommand; import scala.cli.commands.pgp.PgpCommand; @TargetClass(className = "scala.cli.commands.pgp.PgpCommands") public final class PgpCommandsSubst { @Substitute public PgpCommand[] allScalaCommands() { return new PgpCommand[0]; } @Substitute public ExternalCommand[] allExternalCommands() { return new ExternalCommand[] { new PgpCreateExternal(), new PgpKeyIdExternal(), new PgpSignExternal(), new PgpVerifyExternal() }; } } ================================================ FILE: modules/cli/src/main/java/scala/cli/commands/publish/PgpProxyMakerSubst.java ================================================ package scala.cli.internal; import com.oracle.svm.core.annotate.Substitute; import com.oracle.svm.core.annotate.TargetClass; import scala.cli.commands.pgp.PgpProxy; /** Used for choosing the right PGP proxy implementation when Scala CLI is run as a native image. * This class is used to substitute scala.cli.commands.pgp.PgpProxyMaker. * This decouples Scala CLI native image from BouncyCastle used by scala-cli-signing. */ @TargetClass(className = "scala.cli.commands.pgp.PgpProxyMaker") public final class PgpProxyMakerSubst { @Substitute public PgpProxy get(Boolean forceSigningExternally) { return new PgpProxy(); } } ================================================ FILE: modules/cli/src/main/java/scala/cli/internal/Argv0Subst.java ================================================ package scala.cli.internal; import com.oracle.svm.core.annotate.Substitute; import com.oracle.svm.core.annotate.TargetClass; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; import java.nio.file.Path; @TargetClass(className = "scala.cli.internal.Argv0") @Platforms({Platform.LINUX.class, Platform.DARWIN.class}) final class Argv0Subst { @Substitute String get(String defaultValue) { return com.oracle.svm.core.JavaMainWrapper.getCRuntimeArgument0(); } } ================================================ FILE: modules/cli/src/main/java/scala/cli/internal/Argv0SubstWindows.java ================================================ package scala.cli.internal; import com.oracle.svm.core.annotate.Substitute; import com.oracle.svm.core.annotate.TargetClass; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; import java.nio.file.Path; @TargetClass(className = "scala.cli.internal.Argv0") @Platforms({Platform.WINDOWS.class}) final class Argv0SubstWindows { @Substitute String get(String defaultValue) { return coursier.jniutils.ModuleFileName.get(); } } ================================================ FILE: modules/cli/src/main/java/scala/cli/internal/BouncycastleSignerMakerSubst.java ================================================ package scala.cli.internal; import com.oracle.svm.core.annotate.Substitute; import com.oracle.svm.core.annotate.TargetClass; import coursier.publish.signing.Signer; import java.nio.file.Path; import java.util.function.Supplier; import scala.build.Logger; import scala.cli.publish.BouncycastleExternalSigner$; import scala.cli.signing.shared.PasswordOption; /** Used for choosing the right BouncyCastleSigner when Scala CLI is run as a native image. * This class is used to substitute scala.cli.commands.pgp.PgpProxyMaker. * This decouples Scala CLI native image from BouncyCastle used by scala-cli-signing. */ @TargetClass(className = "scala.cli.publish.BouncycastleSignerMaker") public final class BouncycastleSignerMakerSubst { @Substitute public Signer get( Boolean forceSigningExternally, PasswordOption passwordOrNull, PasswordOption secretKey, Supplier command, Logger logger ) { return BouncycastleExternalSigner$.MODULE$.apply(secretKey, passwordOrNull, command.get(), logger); } @Substitute void maybeInit() { // do nothing } } ================================================ FILE: modules/cli/src/main/java/scala/cli/internal/CsJniUtilsFeature.java ================================================ package scala.cli.internal; import com.oracle.svm.core.annotate.AutomaticFeature; import com.oracle.svm.core.jdk.NativeLibrarySupport; import com.oracle.svm.core.jdk.PlatformNativeLibrarySupport; import com.oracle.svm.hosted.FeatureImpl; import com.oracle.svm.hosted.c.NativeLibraries; import org.graalvm.nativeimage.hosted.Feature; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; @AutomaticFeature @Platforms({Platform.WINDOWS.class}) public class CsJniUtilsFeature implements Feature { @Override public void beforeAnalysis(BeforeAnalysisAccess access) { NativeLibrarySupport.singleton().preregisterUninitializedBuiltinLibrary("csjniutils"); PlatformNativeLibrarySupport.singleton().addBuiltinPkgNativePrefix("coursier_bootstrap_launcher_jniutils_"); PlatformNativeLibrarySupport.singleton().addBuiltinPkgNativePrefix("coursier_jniutils_"); PlatformNativeLibrarySupport.singleton().addBuiltinPkgNativePrefix("coursierapi_internal_jniutils_"); PlatformNativeLibrarySupport.singleton().addBuiltinPkgNativePrefix("lmcoursier_internal_jniutils_"); NativeLibraries nativeLibraries = ((FeatureImpl.BeforeAnalysisAccessImpl) access).getNativeLibraries(); nativeLibraries.addStaticJniLibrary("csjniutils"); nativeLibraries.addDynamicNonJniLibrary("ole32"); nativeLibraries.addDynamicNonJniLibrary("shell32"); nativeLibraries.addDynamicNonJniLibrary("Advapi32"); } } ================================================ FILE: modules/cli/src/main/java/scala/cli/internal/LibsodiumjniFeature.java ================================================ package scala.cli.internal; import com.oracle.svm.core.annotate.AutomaticFeature; import com.oracle.svm.core.jdk.NativeLibrarySupport; import com.oracle.svm.core.jdk.PlatformNativeLibrarySupport; import com.oracle.svm.hosted.FeatureImpl; import com.oracle.svm.hosted.c.NativeLibraries; import org.graalvm.nativeimage.hosted.Feature; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; import java.util.Locale; @AutomaticFeature @Platforms({Platform.LINUX.class, Platform.WINDOWS.class}) public class LibsodiumjniFeature implements Feature { @Override public void beforeAnalysis(BeforeAnalysisAccess access) { boolean isWindows = System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("windows"); boolean isStaticLauncher = Boolean.getBoolean("scala-cli.static-launcher"); if (!isWindows && !isStaticLauncher) { System.err.println("Actually disabling LibsodiumjniFeature (not Windows nor static launcher)"); return; } NativeLibrarySupport.singleton().preregisterUninitializedBuiltinLibrary("sodiumjni"); PlatformNativeLibrarySupport.singleton().addBuiltinPkgNativePrefix("libsodiumjni_"); NativeLibraries nativeLibraries = ((FeatureImpl.BeforeAnalysisAccessImpl) access).getNativeLibraries(); nativeLibraries.addStaticNonJniLibrary("sodium"); nativeLibraries.addStaticJniLibrary("sodiumjni"); } } ================================================ FILE: modules/cli/src/main/java/scala/cli/internal/PPrintStringPrefixSubst.java ================================================ package scala.cli.internal; import com.oracle.svm.core.annotate.Substitute; import com.oracle.svm.core.annotate.TargetClass; // Remove once we can use https://github.com/com-lihaoyi/PPrint/pull/80 @TargetClass(className = "pprint.StringPrefix$") final class PPrintStringPrefixSubst { @Substitute String apply(scala.collection.Iterable i) { String name = (new PPrintStringPrefixHelper()).apply((scala.collection.Iterable) i); return name; } } ================================================ FILE: modules/cli/src/main/java/scala/cli/internal/PidSubst.java ================================================ package scala.cli.internal; import com.oracle.svm.core.annotate.Substitute; import com.oracle.svm.core.annotate.TargetClass; import com.oracle.svm.core.posix.headers.Unistd; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; import java.nio.file.Path; @TargetClass(className = "scala.cli.internal.Pid") @Platforms({Platform.LINUX.class, Platform.DARWIN.class}) final class PidSubst { @Substitute Integer get() { return Unistd.getpid(); } } ================================================ FILE: modules/cli/src/main/resources/META-INF/native-image/extras/coursier/reflect-config.json ================================================ [ { "name": "com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allPublicMethods": true, "allDeclaredClasses": true, "allPublicClasses": true }, { "name": "org.apache.commons.compress.archivers.zip.AsiExtraField", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allPublicMethods": true, "allDeclaredClasses": true, "allPublicClasses": true }, { "name": "org.apache.commons.compress.archivers.zip.JarMarker", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allPublicMethods": true, "allDeclaredClasses": true, "allPublicClasses": true }, { "name": "org.apache.commons.compress.archivers.zip.ResourceAlignmentExtraField", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allPublicMethods": true, "allDeclaredClasses": true, "allPublicClasses": true }, { "name": "org.apache.commons.compress.archivers.zip.UnicodeCommentExtraField", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allPublicMethods": true, "allDeclaredClasses": true, "allPublicClasses": true }, { "name": "org.apache.commons.compress.archivers.zip.UnicodePathExtraField", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allPublicMethods": true, "allDeclaredClasses": true, "allPublicClasses": true }, { "name": "org.apache.commons.compress.archivers.zip.X000A_NTFS", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allPublicMethods": true, "allDeclaredClasses": true, "allPublicClasses": true }, { "name": "org.apache.commons.compress.archivers.zip.X0014_X509Certificates", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allPublicMethods": true, "allDeclaredClasses": true, "allPublicClasses": true }, { "name": "org.apache.commons.compress.archivers.zip.X0015_CertificateIdForFile", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allPublicMethods": true, "allDeclaredClasses": true, "allPublicClasses": true }, { "name": "org.apache.commons.compress.archivers.zip.X0016_CertificateIdForCentralDirectory", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allPublicMethods": true, "allDeclaredClasses": true, "allPublicClasses": true }, { "name": "org.apache.commons.compress.archivers.zip.X0017_StrongEncryptionHeader", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allPublicMethods": true, "allDeclaredClasses": true, "allPublicClasses": true }, { "name": "org.apache.commons.compress.archivers.zip.X0019_EncryptionRecipientCertificateList", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allPublicMethods": true, "allDeclaredClasses": true, "allPublicClasses": true }, { "name": "org.apache.commons.compress.archivers.zip.X5455_ExtendedTimestamp", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allPublicMethods": true, "allDeclaredClasses": true, "allPublicClasses": true }, { "name": "org.apache.commons.compress.archivers.zip.X7875_NewUnix", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allPublicMethods": true, "allDeclaredClasses": true, "allPublicClasses": true }, { "name": "org.apache.commons.compress.archivers.zip.Zip64ExtendedInformationExtraField", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allPublicMethods": true, "allDeclaredClasses": true, "allPublicClasses": true } ] ================================================ FILE: modules/cli/src/main/resources/META-INF/native-image/extras/pprint/reflect-config.json ================================================ [ { "name": "scala.collection.AbstractIterable", "methods": [ { "name": "collectionClassName", "parameterTypes": [] } ] } ] ================================================ FILE: modules/cli/src/main/resources/META-INF/native-image/org.virtuslab/scala-cli-core/jni-config.json ================================================ [ { "name": "com.swoval.files.apple.FileEvent", "methods": [ { "name": "", "parameterTypes": [ "java.lang.String", "int" ] } ] }, { "name": "com.swoval.files.apple.FileEventMonitorImpl$WrappedConsumer", "methods": [ { "name": "accept", "parameterTypes": [ "java.lang.Object" ] } ] }, { "name": "java.lang.ClassLoader", "methods": [ { "name": "getPlatformClassLoader", "parameterTypes": [] } ] }, { "name": "java.lang.NoSuchMethodError" }, { "name": "java.lang.String" }, { "name": "sun.management.VMManagementImpl", "fields": [ { "name": "compTimeMonitoringSupport" }, { "name": "currentThreadCpuTimeSupport" }, { "name": "objectMonitorUsageSupport" }, { "name": "otherThreadCpuTimeSupport" }, { "name": "remoteDiagnosticCommandsSupport" }, { "name": "synchronizerUsageSupport" }, { "name": "threadAllocatedMemorySupport" }, { "name": "threadContentionMonitoringSupport" } ] } ] ================================================ FILE: modules/cli/src/main/resources/META-INF/native-image/org.virtuslab/scala-cli-core/native-image.properties ================================================ Args = --no-fallback \ --enable-url-protocols=http,https \ --initialize-at-build-time=com.google.common.jimfs.SystemJimfsFileSystemProvider \ --initialize-at-build-time=com.google.common.base.Preconditions \ -H:IncludeResources=bootstrap.*.jar \ -H:IncludeResources=coursier/coursier.properties \ -H:IncludeResources=coursier/launcher/coursier.properties \ -H:IncludeResources=coursier/launcher/.*.bat \ --report-unsupported-elements-at-runtime \ -H:+ReportExceptionStackTraces \ -Djdk.http.auth.tunneling.disabledSchemes= ================================================ FILE: modules/cli/src/main/resources/META-INF/native-image/org.virtuslab/scala-cli-core/proxy-config.json ================================================ [ [ "bloop.rifle.BuildServer", "org.eclipse.lsp4j.jsonrpc.Endpoint" ], [ "ch.epfl.scala.bsp4j.BuildClient", "org.eclipse.lsp4j.jsonrpc.Endpoint" ] ] ================================================ FILE: modules/cli/src/main/resources/META-INF/native-image/org.virtuslab/scala-cli-core/reflect-config.json ================================================ [ { "name": "bloop.rifle.BuildServer", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "bloop.rifle.bloop4j.BloopExtraBuildParams", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.BspConnectionDetails", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.BuildClient", "queryAllDeclaredMethods": true, "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.BuildClientCapabilities", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.BuildServer", "queryAllDeclaredMethods": true, "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.BuildServerCapabilities", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.BuildTarget", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.BuildTargetCapabilities", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.BuildTargetDataKind", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.BuildTargetEvent", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.BuildTargetEventKind", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.BuildTargetIdentifier", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.BuildTargetTag", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.CleanCacheParams", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.CleanCacheResult", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.CompileParams", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.CompileProvider", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.CompileReport", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.CompileResult", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.CompileTask", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.DebugProvider", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.DebugSessionAddress", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.DebugSessionParams", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.DebugSessionParamsDataKind", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.DependencyModule", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.DependencyModulesItem", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.DependencyModulesParams", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.DependencyModulesResult", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.DependencySourcesItem", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.DependencySourcesParams", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.DependencySourcesResult", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.Diagnostic", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.DiagnosticRelatedInformation", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.DiagnosticSeverity", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.DidChangeBuildTarget", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.InitializeBuildParams", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.InitializeBuildResult", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.InverseSourcesParams", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.InverseSourcesResult", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.JavaBuildServer", "queryAllDeclaredMethods": true, "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.JavacOptionsItem", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.JavacOptionsParams", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.JavacOptionsResult", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.JvmBuildServer", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.JvmBuildTarget", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.JvmEnvironmentItem", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.JvmMainClass", "queryAllDeclaredMethods": true, "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.JvmRunEnvironmentParams", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.JvmRunEnvironmentResult", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.JvmTestEnvironmentParams", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.JvmTestEnvironmentResult", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.Location", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.LogMessageParams", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.MessageType", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.OutputPathItem", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.OutputPathItemKind", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.OutputPathsItem", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.OutputPathsParams", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.OutputPathsResult", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.Position", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.PublishDiagnosticsParams", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.Range", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.ResourcesItem", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.ResourcesParams", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.ResourcesResult", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.RunParams", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.RunParamsDataKind", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.RunProvider", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.RunResult", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.SbtBuildTarget", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.ScalaAction", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.ScalaBuildServer", "queryAllDeclaredMethods": true, "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.ScalaBuildTarget", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.ScalaDiagnostic", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.ScalaMainClass", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.ScalaMainClassesItem", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.ScalaMainClassesParams", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.ScalaMainClassesResult", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.ScalaPlatform", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.ScalaTestClassesItem", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.ScalaTestClassesParams", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.ScalaTestClassesResult", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.ScalaTestParams", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.ScalaTextEdit", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.ScalaWorkspaceEdit", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.ScalacOptionsItem", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.ScalacOptionsParams", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.ScalacOptionsResult", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.ShowMessageParams", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.SourceItem", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.SourceItemKind", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.SourcesItem", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.SourcesParams", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.SourcesResult", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.StatusCode", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.TaskFinishParams", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.TaskId", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.TaskProgressParams", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.TaskStartParams", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.TestFinish", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.TestParams", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.TestParamsDataKind", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.TestProvider", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.TestReport", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.TestResult", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.TestStart", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.TestStatus", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.TestTask", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.TextDocumentIdentifier", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "ch.epfl.scala.bsp4j.WorkspaceBuildTargetsResult", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "com.google.api.client.http.GenericUrl", "allDeclaredFields": true }, { "name": "com.google.api.client.http.HttpHeaders", "allDeclaredFields": true, "allDeclaredMethods": true }, { "name": "com.google.api.client.util.GenericData", "allDeclaredFields": true }, { "name": "com.google.cloud.tools.jib.api.DockerInfoDetails", "allDeclaredFields": true, "allDeclaredMethods": true, "allDeclaredConstructors": true }, { "name": "com.google.cloud.tools.jib.cache.LayerEntriesSelector$LayerEntryTemplate", "allDeclaredFields": true, "allDeclaredMethods": true, "allDeclaredConstructors": true }, { "name": "com.google.cloud.tools.jib.docker.json.DockerManifestEntryTemplate", "allDeclaredFields": true, "allDeclaredMethods": true, "allDeclaredConstructors": true }, { "name": "com.google.cloud.tools.jib.image.json.BuildableManifestTemplate", "allDeclaredMethods": true }, { "name": "com.google.cloud.tools.jib.image.json.BuildableManifestTemplate$ContentDescriptorTemplate", "allDeclaredFields": true, "allDeclaredMethods": true, "allDeclaredConstructors": true }, { "name": "com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate", "allDeclaredFields": true, "allDeclaredMethods": true, "allDeclaredConstructors": true }, { "name": "com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate$ConfigurationObjectTemplate", "allDeclaredFields": true, "allDeclaredMethods": true, "allDeclaredConstructors": true }, { "name": "com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate$HealthCheckObjectTemplate", "allDeclaredFields": true, "allDeclaredMethods": true, "allDeclaredConstructors": true }, { "name": "com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate$RootFilesystemObjectTemplate", "allDeclaredFields": true, "allDeclaredMethods": true, "allDeclaredConstructors": true }, { "name": "com.google.cloud.tools.jib.image.json.DescriptorDigestDeserializer", "methods": [ { "name": "", "parameterTypes": [] } ] }, { "name": "com.google.cloud.tools.jib.image.json.DescriptorDigestSerializer", "methods": [ { "name": "", "parameterTypes": [] } ] }, { "name": "com.google.cloud.tools.jib.image.json.HistoryEntry", "allDeclaredFields": true, "allDeclaredMethods": true, "allDeclaredConstructors": true }, { "name": "com.google.cloud.tools.jib.image.json.ImageMetadataTemplate", "allDeclaredFields": true, "allDeclaredMethods": true, "allDeclaredConstructors": true }, { "name": "com.google.cloud.tools.jib.image.json.ManifestAndConfigTemplate", "allDeclaredFields": true, "allDeclaredMethods": true, "allDeclaredConstructors": true }, { "name": "com.google.cloud.tools.jib.image.json.ManifestTemplate", "allDeclaredMethods": true }, { "name": "com.google.cloud.tools.jib.image.json.OciIndexTemplate", "allDeclaredFields": true, "allDeclaredMethods": true, "allDeclaredConstructors": true }, { "name": "com.google.cloud.tools.jib.image.json.OciIndexTemplate$ManifestDescriptorTemplate", "allDeclaredFields": true, "allDeclaredMethods": true, "allDeclaredConstructors": true }, { "name": "com.google.cloud.tools.jib.image.json.OciIndexTemplate$ManifestDescriptorTemplate$Platform", "allDeclaredFields": true, "allDeclaredMethods": true, "allDeclaredConstructors": true }, { "name": "com.google.cloud.tools.jib.image.json.OciManifestTemplate", "allDeclaredFields": true, "allDeclaredMethods": true, "allDeclaredConstructors": true }, { "name": "com.google.cloud.tools.jib.image.json.V22ManifestListTemplate", "allDeclaredFields": true, "allDeclaredMethods": true, "allDeclaredConstructors": true }, { "name": "com.google.cloud.tools.jib.image.json.V22ManifestListTemplate$ManifestDescriptorTemplate", "allDeclaredFields": true, "allDeclaredMethods": true, "allDeclaredConstructors": true }, { "name": "com.google.cloud.tools.jib.image.json.V22ManifestListTemplate$ManifestDescriptorTemplate$Platform", "allDeclaredFields": true, "allDeclaredMethods": true, "allDeclaredConstructors": true }, { "name": "com.google.cloud.tools.jib.image.json.V22ManifestTemplate", "allDeclaredFields": true, "allDeclaredMethods": true, "allDeclaredConstructors": true }, { "name": "com.google.cloud.tools.jib.json.JsonTemplate", "allDeclaredMethods": true }, { "name": "com.google.cloud.tools.jib.registry.RegistryAuthenticator$AuthenticationResponseTemplate", "allDeclaredFields": true, "allDeclaredMethods": true, "allDeclaredConstructors": true }, { "name": "com.google.common.jimfs.SystemJimfsFileSystemProvider", "methods": [ { "name": "removeFileSystemRunnable", "parameterTypes": [ "java.net.URI" ] } ] }, { "name": "coursier.cache.loggers.RefreshLogger", "allDeclaredFields": true }, { "name": "java.lang.String" }, { "name": "java.util.List", "allDeclaredMethods": true }, { "name": "org.apache.commons.logging.LogFactory" }, { "name": "org.apache.commons.logging.impl.LogFactoryImpl", "methods": [ { "name": "", "parameterTypes": [] } ] }, { "name": "org.apache.commons.logging.impl.NoOpLog", "methods": [ { "name": "", "parameterTypes": [ "java.lang.String" ] } ] }, { "name": "org.apache.commons.logging.impl.WeakHashtable", "methods": [ { "name": "", "parameterTypes": [] } ] }, { "name": "org.eclipse.jgit.internal.JGitText", "allPublicFields": true, "methods": [ { "name": "", "parameterTypes": [] } ] }, { "name": "org.eclipse.jgit.lib.CoreConfig$LogRefUpdates", "methods": [ { "name": "values", "parameterTypes": [] } ] }, { "name": "org.eclipse.jgit.lib.CoreConfig$TrustPackedRefsStat", "methods": [ { "name": "values", "parameterTypes": [] } ] }, { "name": "org.eclipse.jgit.lib.CoreConfig$TrustStat", "methods": [ { "name": "values", "parameterTypes": [] } ] }, { "name": "org.eclipse.lsp4j.jsonrpc.RemoteEndpoint", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "org.eclipse.lsp4j.jsonrpc.json.MessageConstants", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "org.eclipse.lsp4j.jsonrpc.json.MessageJsonHandler", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "org.eclipse.lsp4j.jsonrpc.json.MethodProvider", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "org.eclipse.lsp4j.jsonrpc.json.adapters.JsonElementTypeAdapter$Factory", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "org.eclipse.lsp4j.jsonrpc.messages.CancelParams", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "org.eclipse.lsp4j.jsonrpc.messages.Either", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "org.eclipse.lsp4j.jsonrpc.messages.Message", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "org.eclipse.lsp4j.jsonrpc.messages.MessageIssue", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "org.eclipse.lsp4j.jsonrpc.messages.NotificationMessage", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "org.eclipse.lsp4j.jsonrpc.messages.RequestMessage", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "org.eclipse.lsp4j.jsonrpc.messages.ResponseError", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "org.scalajs.jsenv.ExternalJSRun", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "scala.build.BloopBuildClient", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "scala.build.ConsoleBloopBuildClient", "queryAllDeclaredMethods": true }, { "name": "scala.build.bsp.BspClient", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "scala.build.bsp.BspImpl$LoggingBspClient", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "scala.build.bsp.BspServer", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "scala.build.bsp.BuildClientForwardStubs", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "scala.build.bsp.BuildServerForwardStubs", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "scala.build.bsp.JavaBuildServerForwardStubs", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "scala.build.bsp.JvmBuildServerForwardStubs", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "scala.build.bsp.LoggingBuildClient", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "scala.build.bsp.LoggingBuildServer", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "scala.build.bsp.LoggingJavaBuildServer", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "scala.build.bsp.LoggingJvmBuildServer", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "scala.build.bsp.LoggingScalaBuildServer", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "scala.build.bsp.ScalaBuildServerForwardStubs", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "scala.build.bsp.ScalaScriptBuildServer", "queryAllDeclaredMethods": true, "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "scala.build.bsp.WrappedSourceItem", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "scala.build.bsp.WrappedSourcesItem", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "scala.build.bsp.WrappedSourcesParams", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "scala.build.bsp.WrappedSourcesResult", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true }, { "name": "scala.build.bsp.protocol.TextEdit", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true } ] ================================================ FILE: modules/cli/src/main/resources/META-INF/native-image/org.virtuslab/scala-cli-core/resource-config.json ================================================ { "resources": { "includes": [ { "pattern": "\\QMETA-INF/services/java.nio.file.spi.FileSystemProvider\\E" }, { "pattern": "\\QMETA-INF/services/org.slf4j.spi.SLF4JServiceProvider\\E" }, { "pattern": "\\QMETA-INF/services/org.xnio.XnioProvider\\E" }, { "pattern": "\\Qlibrary.properties\\E" }, { "pattern": "\\Qnative/x86_64/libswoval-files0.dylib\\E" }, { "pattern": "\\Qcommons-logging.properties\\E" }, { "pattern": ".*scala3RuntimeFixes.jar$" }, { "pattern": ".*JGitText.properties$" } ] }, "bundles": [] } ================================================ FILE: modules/cli/src/main/scala/coursier/CoursierUtil.scala ================================================ package coursier import coursier.util.WebPage object CoursierUtil { def rawVersions(repoUrl: String, page: String) = WebPage.listDirectories(repoUrl, page) } ================================================ FILE: modules/cli/src/main/scala/scala/cli/CurrentParams.scala ================================================ package scala.cli // Kind of meh to keep stuff in a global mutable state like this. // This is only used by the stacktrace persisting stuff in ScalaCli.main object CurrentParams { var workspaceOpt = Option.empty[os.Path] var verbosity = 0 } ================================================ FILE: modules/cli/src/main/scala/scala/cli/ScalaCli.scala ================================================ package scala.cli import bloop.rifle.FailedToStartServerException import coursier.version.Version import sun.misc.{Signal, SignalHandler} import java.io.{ByteArrayOutputStream, PrintStream} import java.nio.charset.StandardCharsets import java.nio.file.Paths import java.util.Locale import scala.build.EitherCps.{either, value} import scala.build.internal.Constants import scala.build.internals.EnvVar import scala.cli.commands.CommandUtils import scala.cli.config.Keys import scala.cli.internal.Argv0 import scala.cli.javaLauncher.JavaLauncherCli import scala.cli.launcher.{LauncherCli, LauncherOptions, PowerOptions} import scala.cli.publish.BouncycastleSignerMaker import scala.cli.util.ConfigDbUtils import scala.util.Properties object ScalaCli { if (Properties.isWin && isGraalvmNativeImage) // have to be initialized before running (new Argv0).get because Argv0SubstWindows uses csjniutils library // The DLL loaded by LoadWindowsLibrary is statically linke/d in // the Scala CLI native image, no need to manually load it. coursier.jniutils.LoadWindowsLibrary.assumeInitialized() private val defaultProgName = "scala-cli" var progName: String = { val argv0 = (new Argv0).get(defaultProgName) val last = Paths.get(argv0).getFileName.toString last match { case s".${name}.aux" => name // cs installs binaries under .app-name.aux and adds them to the PATH case _ => argv0 } } private val scalaCliBinaryName = "scala-cli" private var isSipScala = { lazy val isPowerConfigDb = for { configDb <- ConfigDbUtils.configDb.toOption powerEntry <- configDb.get(Keys.power).toOption power <- powerEntry } yield power val isPowerEnv = EnvVar.ScalaCli.power.valueOpt.flatMap(_.toBooleanOption) val isPower = isPowerEnv.orElse(isPowerConfigDb).getOrElse(false) !isPower } def setPowerMode(power: Boolean): Unit = isSipScala = !power def allowRestrictedFeatures = !isSipScala def fullRunnerName = if (progName.contains(scalaCliBinaryName)) "Scala CLI" else "Scala code runner" def baseRunnerName = if (progName.contains(scalaCliBinaryName)) scalaCliBinaryName else "scala" private def isGraalvmNativeImage: Boolean = sys.props.contains("org.graalvm.nativeimage.imagecode") private var maybeLauncherOptions: Option[LauncherOptions] = None def launcherOptions: LauncherOptions = maybeLauncherOptions.getOrElse(LauncherOptions()) def getDefaultScalaVersion: String = launcherOptions.scalaRunner.cliUserScalaVersion.getOrElse(Constants.defaultScalaVersion) private var launcherJavaPropArgs: List[String] = List.empty def getLauncherJavaPropArgs: List[String] = launcherJavaPropArgs private def partitionArgs(args: Array[String]): (Array[String], Array[String]) = { val systemProps = args.takeWhile(_.startsWith("-D")) (systemProps, args.drop(systemProps.length)) } private def setSystemProps(systemProps: Array[String]): Unit = { systemProps.map(_.stripPrefix("-D")).foreach { prop => prop.split("=", 2) match { case Array(key, value) => System.setProperty(key, value) case Array(key) => System.setProperty(key, "") } } } private def printThrowable(t: Throwable, out: PrintStream): Unit = if (t != null) { out.println(t.toString) // FIXME Print t.getSuppressed too? for (l <- t.getStackTrace) out.println(s" $l") printThrowable(t.getCause, out) } private def printThrowable(t: Throwable): Array[Byte] = { val baos = new ByteArrayOutputStream val ps = new PrintStream(baos, true, StandardCharsets.UTF_8.name()) printThrowable(t, ps) baos.toByteArray } private def isCI = EnvVar.Internal.ci.valueOpt.nonEmpty private def printStackTraces = EnvVar.ScalaCli.printStackTraces.valueOpt .map(_.toLowerCase(Locale.ROOT)) .exists { case "true" | "1" => true case _ => false } private def ignoreSigpipe(): Unit = Signal.handle(new Signal("PIPE"), SignalHandler.SIG_IGN) private def isJava17ClassName(name: String): Boolean = name == "java/net/UnixDomainSocketAddress" private lazy val javaMajorVersion = sys.props.getOrElse("java.version", "0") .stripPrefix("1.") .takeWhile(_.isDigit) .toInt def main(args: Array[String]): Unit = try main0(args) catch { case e: Throwable if !isCI && !printStackTraces => val workspace = CurrentParams.workspaceOpt.filter(os.isDir).getOrElse(os.pwd) val dir = workspace / Constants.workspaceDirName / "stacktraces" os.makeDir.all(dir) import java.time.Instant val tempFile = os.temp( contents = printThrowable(e), dir = dir, prefix = Instant.now().getEpochSecond().toString() + "-", suffix = ".log", deleteOnExit = false ) if (CurrentParams.verbosity <= 1) { System.err.println(s"Error: $e") System.err.println(s"For more details, please see '$tempFile'") } e match { case _: UnsupportedClassVersionError if javaMajorVersion < Constants.minimumBloopJavaVersion => warnRequiresMinimumBloopJava() case _: NoClassDefFoundError if isJava17ClassName(e.getMessage) && CurrentParams.verbosity <= 1 && javaMajorVersion < Constants.minimumInternalJavaVersion => // Actually Java >= 16 here, but let's recommend a LTS version… warnRequiresMinimumBloopJava() case _: FailedToStartServerException => System.err.println( s"""Running | $progName --power bloop output |might give more details.""".stripMargin ) case ex: java.util.zip.ZipException if !Properties.isWin && ex.getMessage.contains("invalid entry CRC") => // Suggest workaround of https://github.com/VirtusLab/scala-cli/pull/865 // for https://github.com/VirtusLab/scala-cli/issues/828 System.err.println( s"""Running | export ${EnvVar.ScalaCli.vendoredZipInputStream.name}=true |before running $fullRunnerName might fix the issue. |""".stripMargin ) case _ => } if (CurrentParams.verbosity >= 2) throw e else sys.exit(1) } private def warnRequiresMinimumBloopJava(): Unit = System.err.println( s"Java >= ${Constants.minimumBloopJavaVersion} is required to run $fullRunnerName (found Java $javaMajorVersion)" ) def loadJavaProperties(cwd: os.Path) = { // load java properties from scala-cli-properties resource file val prop = new java.util.Properties() val cl = getClass.getResourceAsStream("/java-properties/scala-cli-properties") if cl != null then prop.load(cl) prop.stringPropertyNames().forEach(name => System.setProperty(name, prop.getProperty(name))) // load java properties from .scala-jvmopts located in the current working directory and filter only java properties and warning if someone used other options val jvmopts = cwd / Constants.jvmPropertiesFileName if os.exists(jvmopts) && os.isFile(jvmopts) then val jvmoptsContent = os.read(jvmopts) val jvmoptsLines = jvmoptsContent.linesIterator.toSeq val (javaOpts, otherOpts) = jvmoptsLines.partition(_.startsWith("-D")) javaOpts.foreach { opt => opt.stripPrefix("-D").split("=", 2) match { case Array(key, value) => System.setProperty(key, value) case _ => System.err.println(s"Warning: Invalid java property: $opt") } } if otherOpts.nonEmpty then System.err.println( s"Warning: Only java properties are supported in .scala-jvmopts file. Other options are ignored: ${otherOpts.mkString(", ")}" ) // load java properties from config for { configDb <- ConfigDbUtils.configDb.toOption properties <- configDb.get(Keys.javaProperties).getOrElse(Nil).iterator } properties.foreach { opt => opt.stripPrefix("-D").split("=", 2) match { case Array(key, value) => System.setProperty(key, value) case _ => System.err.println(s"Warning: Invalid java property in config: $opt") } } // load java properties from JAVA_OPTS and JDK_JAVA_OPTIONS environment variables val javaOpts: Seq[String] = EnvVar.Java.javaOpts.valueOpt.toSeq ++ EnvVar.Java.jdkJavaOpts.valueOpt.toSeq val ignoredJavaOpts = javaOpts .flatMap(_.split("\\s+")) .flatMap { opt => opt.stripPrefix("-D").split("=", 2) match { case Array(key, value) => System.setProperty(key, value) None case ignored => Some(ignored) // non-property opts are ignored here } }.flatten if ignoredJavaOpts.nonEmpty then System.err.println( s"Warning: Only java properties are supported in ${EnvVar.Java.javaOpts.name} and ${EnvVar .Java.jdkJavaOpts.name} environment variables. Other options are ignored: ${ignoredJavaOpts.mkString(", ")}" ) } private def main0(args: Array[String]): Unit = either { loadJavaProperties(cwd = os.pwd) // load java properties to detect launcher kind val remainingArgs = value { LauncherOptions.parser.stopAtFirstUnrecognized.parse(args.toVector) match { case Left(e) => System.err.println(e.message) sys.exit(1) case Right((launcherOpts, args0)) => maybeLauncherOptions = Some(launcherOpts) launcherOpts.cliVersion.map(_.trim).filter(_.nonEmpty) match { case Some(ver) => val powerArgs = launcherOpts.powerOptions.toCliArgs val initialScalaRunnerArgs = launcherOpts.scalaRunner val finalScalaRunnerArgs = (Version(ver) match case v if v < Version("1.4.0") && !ver.contains("nightly") => initialScalaRunnerArgs.copy( skipCliUpdates = None, predefinedCliVersion = None, initialLauncherPath = None ) case v if v < Version("1.5.0-34-g31a88e428-SNAPSHOT") && v < Version("1.5.1") && !ver.contains("nightly") => initialScalaRunnerArgs.copy( predefinedCliVersion = None, initialLauncherPath = None ) case _ if initialScalaRunnerArgs.initialLauncherPath.nonEmpty => initialScalaRunnerArgs case _ => initialScalaRunnerArgs.copy( predefinedCliVersion = Some(ver), initialLauncherPath = Some(CommandUtils.getAbsolutePathToScalaCli(progName)) ) ).toCliArgs val newArgs = powerArgs ++ finalScalaRunnerArgs ++ args0 LauncherCli.runAndExit(ver, launcherOpts, newArgs) case _ if javaMajorVersion < Constants.minimumLauncherJavaVersion && sys.props.get("scala-cli.kind").exists(_.startsWith("jvm")) => System.err.println( s"[${Console.RED}error${Console.RESET}] Java $javaMajorVersion is not supported with this Scala CLI (JVM) launcher." ) System.err.println( s"[${Console.RED}error${Console.RESET}] Please upgrade to at least Java ${Constants.minimumLauncherJavaVersion} or use a native Scala CLI launcher instead." ) sys.exit(1) case _ if javaMajorVersion >= Constants.minimumLauncherJavaVersion && javaMajorVersion < Constants.minimumBloopJavaVersion && sys.props.get("scala-cli.kind").exists(_.startsWith("jvm")) => JavaLauncherCli.runAndExit(args.toSeq) case None => launcherOpts.scalaRunner.progName .foreach(pn => progName = pn) if launcherOpts.powerOptions.power then isSipScala = false Right(args0.toArray) else // Parse again to register --power at any position // Don't consume it, GlobalOptions parsing will do it PowerOptions.parser.ignoreUnrecognized.parse(args0) match { case Right((powerOptions, _)) => if powerOptions.power then isSipScala = false Right(args0.toArray) case Left(e) => System.err.println(e.message) sys.exit(1) } } } } val (systemProps, scalaCliArgs) = partitionArgs(remainingArgs) if systemProps.nonEmpty then launcherJavaPropArgs = systemProps.toList setSystemProps(systemProps) (new BouncycastleSignerMaker).maybeInit() coursier.Resolve.proxySetup() // Getting killed by SIGPIPE quite often when on musl (in the "static" native // image), but also sometimes on glibc, or even on macOS, when we use domain // sockets to exchange with Bloop. So let's just ignore those (which should // just make some read / write calls return -1). if (!Properties.isWin && isGraalvmNativeImage) ignoreSigpipe() if (Properties.isWin && System.console() != null && coursier.paths.Util.useJni()) // Enable ANSI output in Windows terminal try coursier.jniutils.WindowsAnsiTerminal.enableAnsiOutput() catch { // ignore error resulting from redirect STDOUT to /dev/null case e: java.io.IOException if e.getMessage.contains("GetConsoleMode error 6") => } new ScalaCliCommands(progName, baseRunnerName, fullRunnerName) .main(scalaCliArgs) } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/ScalaCliCommands.scala ================================================ package scala.cli import caseapp.core.app.CommandsEntryPoint import caseapp.core.help.{Help, HelpFormat, RuntimeCommandsHelp} import java.nio.file.InvalidPathException import scala.cli.commands.* import scala.cli.commands.shared.ScalaCliHelp class ScalaCliCommands( val progName: String, baseRunnerName: String, fullRunnerName: String ) extends CommandsEntryPoint { lazy val actualDefaultCommand = new default.Default(help) // for debugging purposes - allows to run the scala-cli-signing binary from the Scala CLI JVM launcher private lazy val pgpUseBinaryCommands = java.lang.Boolean.getBoolean("scala-cli.pgp.binary-commands") private def pgpCommands = new pgp.PgpCommands private def pgpBinaryCommands = new pgp.PgpCommandsSubst private def allCommands = Seq[ScalaCommand[?]]( addpath.AddPath, bloop.Bloop, bloop.BloopExit, bloop.BloopOutput, bloop.BloopStart, bsp.Bsp, clean.Clean, compile.Compile, config.Config, default.DefaultFile, dependencyupdate.DependencyUpdate, directories.Directories, doc.Doc, export0.Export, fix.Fix, fmt.Fmt, new HelpCmd(help), installcompletions.InstallCompletions, installhome.InstallHome, `new`.New, repl.Repl, package0.Package, pgp.PgpPull, pgp.PgpPush, publish.Publish, publish.PublishLocal, publish.PublishSetup, run.Run, github.SecretCreate, github.SecretList, setupide.SetupIde, shebang.Shebang, test.Test, uninstall.Uninstall, uninstallcompletions.UninstallCompletions, update.Update, version.Version ) ++ (if (pgpUseBinaryCommands) Nil else pgpCommands.allScalaCommands.toSeq) ++ (if (pgpUseBinaryCommands) pgpBinaryCommands.allScalaCommands.toSeq else Nil) def commands = allCommands ++ (if (pgpUseBinaryCommands) Nil else pgpCommands.allExternalCommands.toSeq) ++ (if (pgpUseBinaryCommands) pgpBinaryCommands.allExternalCommands.toSeq else Nil) override def description: String = { val coreFeaturesString = if ScalaCli.allowRestrictedFeatures then "compile, run, test and package" else "compile, run and test" s"$fullRunnerName is a command-line tool to interact with the Scala language. It lets you $coreFeaturesString your Scala code." } override def summaryDesc = s"""|See '$baseRunnerName --help' to read about a specific subcommand. To see full help run '$baseRunnerName --help-full'. | |To use launcher options, specify them before any other argument. |For example, to run another $fullRunnerName version, specify it with the '--cli-version' launcher option: | ${Console.BOLD}$baseRunnerName --cli-version args${Console.RESET}""".stripMargin final override def defaultCommand = Some(actualDefaultCommand) // FIXME Report this in case-app default NameFormatter override lazy val help: RuntimeCommandsHelp = { val parent = super.help parent.copy(defaultHelp = Help[Unit]()) } override def enableCompleteCommand = true override def enableCompletionsCommand = true override def helpFormat: HelpFormat = ScalaCliHelp.helpFormat private def isShebangFile(arg: String): Boolean = { val pathOpt = try Some(os.Path(arg, os.pwd)) catch { case _: InvalidPathException => None } pathOpt.filter(os.isFile(_)).filter(_.toIO.canRead).exists { path => val content = os.read(path) // FIXME Charset? content.startsWith(s"#!/usr/bin/env $progName" + System.lineSeparator()) } } override def main(args: Array[String]): Unit = { // quick hack, until the raw args are kept in caseapp.RemainingArgs by case-app actualDefaultCommand.rawArgs = args commands.foreach { case c: NeedsArgvCommand => c.setArgv(progName +: args) case _ => } actualDefaultCommand.setArgv(progName +: args) val processedArgs = if (args.lengthCompare(1) > 0 && isShebangFile(args(0))) Array(args(0), "--") ++ args.tail else args super.main(processedArgs) } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/CommandUtils.scala ================================================ package scala.cli.commands import java.io.File import java.nio.file.{Files, Paths} import scala.build.Os import scala.cli.ScalaCli import scala.cli.internal.ProcUtil import scala.util.Try object CommandUtils { def isOutOfDateVersion(newVersion: String, oldVersion: String): Boolean = { import coursier.core.Version Version(newVersion) > Version(oldVersion) } // Ensure the path to the CLI is absolute def getAbsolutePathToScalaCli(programName: String): String = if (programName.replace('\\', '/').contains("/")) os.Path(programName, Os.pwd).toString else /* In order to get absolute path we first try to get it from coursier.mainJar (this works for standalone launcher) If this fails we fallback to getting it from this class and finally we may also use rawArg if there is nothing left */ sys.props.get("coursier.mainJar") .map(Paths.get(_).toAbsolutePath.toString) .orElse { val scalaCliPathsOnPATH = ProcUtil.findApplicationPathsOnPATH(ScalaCli.progName) /* https://github.com/VirtusLab/scala-cli/issues/1048 scalaCLICanonicalPathFromPATH is a map consisting of canonical Scala CLI paths for each symlink find on PATH. If the current launcher path is the same as the canonical Scala CLI path, we use a related symlink that targets to current launcher path. */ val scalaCLICanonicalPathsFromPATH = scalaCliPathsOnPATH .map(path => (os.followLink(os.Path(path, os.pwd)), path)) .collect { case (Some(canonicalPath), symlinkPath) => (canonicalPath, symlinkPath) }.toMap val currentLauncherPathOpt = Try( // This is weird but on windows we get /D:\a\scala-cli... Paths.get(getClass.getProtectionDomain.getCodeSource.getLocation.toURI) .toAbsolutePath .toString ).toOption currentLauncherPathOpt.map(currentLauncherPath => scalaCLICanonicalPathsFromPATH .getOrElse(os.Path(currentLauncherPath), currentLauncherPath) ) } .getOrElse(programName) lazy val shouldCheckUpdate: Boolean = scala.util.Random.nextInt() % 10 == 1 def printablePath(path: os.Path): String = if (path.startsWith(Os.pwd)) "." + File.separator + path.relativeTo(Os.pwd).toString else path.toString extension (launcher: os.Path) { def isJar: Boolean = if os.isFile(launcher) then val mimeType = Files.probeContentType(launcher.toNIO) mimeType match case "application/java-archive" | "application/x-java-archive" => true case "application/zip" => // Extra check: ensure META-INF/MANIFEST.MF exists inside val jarFile = new java.util.jar.JarFile(launcher.toIO) try jarFile.getEntry("META-INF/MANIFEST.MF") != null finally jarFile.close() case _ => false else false def hasSelfExecutablePreamble: Boolean = { // Read first 2 bytes raw: look for shebang '#!' val in = Files.newInputStream(launcher.toNIO) try val b1 = in.read() val b2 = in.read() b1 == '#' && b2 == '!' finally in.close() } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/CustomWindowsEnvVarUpdater.scala ================================================ package scala.cli.commands import coursier.env.* // Only using this instead of coursier.env.WindowsEnvVarUpdater for the "\u0000" striping thing, // that earlier version of the Scala CLI may have left behind. // We should be able to switch back to coursier.env.WindowsEnvVarUpdater // after a bit of time (once super early users used this code more). case class CustomWindowsEnvVarUpdater( powershellRunner: PowershellRunner = PowershellRunner(), useJni: Option[Boolean] = None ) extends EnvVarUpdater { def withUseJni(opt: Option[Boolean]) = copy(useJni = opt) private lazy val useJni0 = useJni.getOrElse { // FIXME Should be coursier.paths.Util.useJni(), but it's not available from here. !System.getProperty("coursier.jni", "").equalsIgnoreCase("false") } // https://stackoverflow.com/questions/9546324/adding-directory-to-path-environment-variable-in-windows/29109007#29109007 // https://docs.microsoft.com/fr-fr/dotnet/api/system.environment.getenvironmentvariable?view=netframework-4.8#System_Environment_GetEnvironmentVariable_System_String_System_EnvironmentVariableTarget_ // https://docs.microsoft.com/fr-fr/dotnet/api/system.environment.setenvironmentvariable?view=netframework-4.8#System_Environment_SetEnvironmentVariable_System_String_System_String_System_EnvironmentVariableTarget_ private def getEnvironmentVariable(name: String): Option[String] = if (useJni0) Option(coursier.jniutils.WindowsEnvironmentVariables.get(name)) else { val output = powershellRunner .runScript(CustomWindowsEnvVarUpdater.getEnvVarScript(name)) .stripSuffix(System.lineSeparator()) if (output == "null") // if ever the actual value is "null", we'll miss it None else Some(output) } private def setEnvironmentVariable(name: String, value: String): Unit = if (useJni0) { val value0 = if (value.contains("\u0000")) value.split(';').filter(!_.contains("\u0000")).mkString(";") else value coursier.jniutils.WindowsEnvironmentVariables.set(name, value0) } else powershellRunner.runScript(CustomWindowsEnvVarUpdater.setEnvVarScript(name, value)) private def clearEnvironmentVariable(name: String): Unit = if (useJni0) coursier.jniutils.WindowsEnvironmentVariables.delete(name) else powershellRunner.runScript(CustomWindowsEnvVarUpdater.clearEnvVarScript(name)) def applyUpdate(update: EnvironmentUpdate): Boolean = { // Beware, these are not an atomic operation overall // (we might discard values added by others between our get and our set) var setSomething = false for ((k, v) <- update.set) { val formerValueOpt = getEnvironmentVariable(k) val needsUpdate = formerValueOpt.forall(_ != v) if (needsUpdate) { setEnvironmentVariable(k, v) setSomething = true } } for ((k, v) <- update.pathLikeAppends) { val formerValueOpt = getEnvironmentVariable(k) val alreadyInList = formerValueOpt .exists(_.split(CustomWindowsEnvVarUpdater.windowsPathSeparator).contains(v)) if (!alreadyInList) { val newValue = formerValueOpt .fold(v)(_ + CustomWindowsEnvVarUpdater.windowsPathSeparator + v) setEnvironmentVariable(k, newValue) setSomething = true } } setSomething } def tryRevertUpdate(update: EnvironmentUpdate): Boolean = { // Beware, these are not an atomic operation overall // (we might discard values added by others between our get and our set) var setSomething = false for ((k, v) <- update.set) { val formerValueOpt = getEnvironmentVariable(k) val wasUpdated = formerValueOpt.exists(_ == v) if (wasUpdated) { clearEnvironmentVariable(k) setSomething = true } } for ((k, v) <- update.pathLikeAppends; formerValue <- getEnvironmentVariable(k)) { val parts = formerValue.split(CustomWindowsEnvVarUpdater.windowsPathSeparator) val isInList = parts.contains(v) if (isInList) { val newValue = parts.filter(_ != v) if (newValue.isEmpty) clearEnvironmentVariable(k) else setEnvironmentVariable( k, newValue.mkString(CustomWindowsEnvVarUpdater.windowsPathSeparator) ) setSomething = true } } setSomething } } object CustomWindowsEnvVarUpdater { private def getEnvVarScript(name: String): String = s"""[Environment]::GetEnvironmentVariable("$name", "User") |""".stripMargin private def setEnvVarScript(name: String, value: String): String = // FIXME value might need some escaping here s"""[Environment]::SetEnvironmentVariable("$name", "$value", "User") |""".stripMargin private def clearEnvVarScript(name: String): String = // FIXME value might need some escaping here s"""[Environment]::SetEnvironmentVariable("$name", $$null, "User") |""".stripMargin private def windowsPathSeparator: String = ";" } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/HelpCmd.scala ================================================ package scala.cli.commands import caseapp.* import caseapp.core.help.RuntimeCommandsHelp import scala.build.Logger import scala.cli.commands.shared.HelpOptions class HelpCmd(actualHelp: => RuntimeCommandsHelp) extends ScalaCommandWithCustomHelp[HelpOptions](actualHelp) { override def names = List(List("help")) override def scalaSpecificationLevel = SpecificationLevel.IMPLEMENTATION override def runCommand(options: HelpOptions, args: RemainingArgs, logger: Logger): Unit = customHelpAsked(showHidden = false) } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/NeedsArgvCommand.scala ================================================ package scala.cli.commands trait NeedsArgvCommand { def setArgv(argv: Array[String]): Unit } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/OptionsHelper.scala ================================================ package scala.cli.commands object OptionsHelper { implicit class Mandatory[A](x: Option[A]) { def mandatory(parameter: String, group: String): A = x match { case Some(v) => v case None => System.err.println( s"${parameter.toLowerCase.capitalize} parameter is mandatory for ${group.toLowerCase.capitalize}" ) sys.exit(1) } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/RestrictableCommand.scala ================================================ package scala.cli.commands import caseapp.core.app.Command import caseapp.core.parser.Parser import scala.build.Logger import scala.build.input.ScalaCliInvokeData trait RestrictableCommand[T](implicit myParser: Parser[T]) { self: Command[T] => def shouldSuppressExperimentalFeatureWarnings: Boolean def shouldSuppressDeprecatedFeatureWarnings: Boolean def logger: Logger protected def invokeData: ScalaCliInvokeData override def parser: Parser[T] = RestrictedCommandsParser( parser = myParser, logger = logger, shouldSuppressExperimentalWarnings = shouldSuppressExperimentalFeatureWarnings, shouldSuppressDeprecatedWarnings = shouldSuppressDeprecatedFeatureWarnings )(using invokeData) final def isRestricted: Boolean = scalaSpecificationLevel == SpecificationLevel.RESTRICTED final def isExperimental: Boolean = scalaSpecificationLevel == SpecificationLevel.EXPERIMENTAL /** Is that command a MUST / SHOULD / NICE TO have for the Scala runner specification? */ def scalaSpecificationLevel: SpecificationLevel // To reduce imports... protected def SpecificationLevel = scala.cli.commands.SpecificationLevel } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/RestrictedCommandsParser.scala ================================================ package scala.cli.commands import caseapp.Name import caseapp.core.parser.Parser import caseapp.core.util.Formatter import caseapp.core.{Arg, Error} import scala.build.Logger import scala.build.input.ScalaCliInvokeData import scala.build.internals.FeatureType import scala.cli.util.ArgHelpers.* object RestrictedCommandsParser { def apply[T]( parser: Parser[T], logger: Logger, shouldSuppressExperimentalWarnings: Boolean, shouldSuppressDeprecatedWarnings: Boolean )(using ScalaCliInvokeData): Parser[T] = new Parser[T] { type D = parser.D def args: Seq[caseapp.core.Arg] = parser.args.filter(_.isSupported) def get( d: D, nameFormatter: caseapp.core.util.Formatter[caseapp.Name] ): Either[caseapp.core.Error, T] = parser.get(d, nameFormatter) def init: D = parser.init def withDefaultOrigin(origin: String): caseapp.core.parser.Parser[T] = RestrictedCommandsParser( parser.withDefaultOrigin(origin), logger, shouldSuppressExperimentalWarnings, shouldSuppressDeprecatedWarnings ) override def step( args: List[String], index: Int, d: D, nameFormatter: Formatter[Name] ): Either[(Error, Arg, List[String]), Option[(D, Arg, List[String])]] = (parser.step(args, index, d, nameFormatter), args) match { case (Right(Some(_, arg: Arg, _)), passedOption :: _) if !arg.isSupported => Left(( Error.UnrecognizedArgument(arg.powerOptionUsedInSip(passedOption)), arg, Nil )) case (r @ Right(Some(_, arg: Arg, _)), passedOption :: _) if arg.isExperimental && !shouldSuppressExperimentalWarnings => logger.experimentalWarning(passedOption, FeatureType.Option) r case (r @ Right(Some(_, arg: Arg, _)), passedOption :: _) if arg.isDeprecated && !shouldSuppressDeprecatedWarnings => // TODO implement proper deprecation logic: https://github.com/VirtusLab/scala-cli/issues/3258 arg.deprecatedOptionAliases.find(_ == passedOption) .foreach { deprecatedAlias => logger.message( s"""[${Console.YELLOW}warn${Console.RESET}] The $deprecatedAlias option alias has been deprecated and may be removed in a future version.""" ) } r case (other, _) => other } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/ScalaCommand.scala ================================================ package scala.cli.commands import caseapp.core.app.Command import caseapp.core.complete.{Completer, CompletionItem} import caseapp.core.help.{Help, HelpFormat} import caseapp.core.parser.Parser import caseapp.core.util.Formatter import caseapp.core.{Arg, Error, RemainingArgs} import caseapp.{HelpMessage, Name} import dependency.* import java.util.concurrent.atomic.AtomicReference import scala.annotation.tailrec import scala.build.EitherCps.{either, value} import scala.build.compiler.SimpleScalaCompiler import scala.build.errors.BuildException import scala.build.input.{ScalaCliInvokeData, SubCommand} import scala.build.internal.util.WarningMessages import scala.build.internal.{Constants, Runner} import scala.build.internals.{EnvVar, FeatureType} import scala.build.options.ScalacOpt.noDashPrefixes import scala.build.options.{BuildOptions, Scope} import scala.build.{Artifacts, Logger, Positioned, ReplArtifacts} import scala.cli.commands.default.LegacyScalaOptions import scala.cli.commands.shared.* import scala.cli.commands.util.CommandHelpers import scala.cli.commands.util.ScalacOptionsUtil.* import scala.cli.config.Keys import scala.cli.internal.ProcUtil import scala.cli.launcher.LauncherOptions import scala.cli.util.ConfigDbUtils.* import scala.cli.{CurrentParams, ScalaCli} abstract class ScalaCommand[T <: HasGlobalOptions](implicit myParser: Parser[T], inHelp: Help[T]) extends Command()(using myParser, inHelp) with NeedsArgvCommand with CommandHelpers with RestrictableCommand[T] { private val globalOptionsAtomic: AtomicReference[GlobalOptions] = new AtomicReference(GlobalOptions.default) private def globalOptions: GlobalOptions = globalOptionsAtomic.get() protected def launcherOptions: LauncherOptions = ScalaCli.launcherOptions protected def defaultScalaVersion: String = ScalaCli.getDefaultScalaVersion protected def launcherJavaPropArgs: List[String] = ScalaCli.getLauncherJavaPropArgs def sharedOptions(t: T): Option[SharedOptions] = // hello borked unused warning None override def hasFullHelp = true override def hidden: Boolean = shouldExcludeInSip protected var argvOpt = Option.empty[Array[String]] protected def allowRestrictedFeatures: Boolean = ScalaCli.allowRestrictedFeatures || globalOptions.powerOptions.power private def shouldExcludeInSip = (isRestricted || isExperimental) && !allowRestrictedFeatures override def setArgv(argv: Array[String]): Unit = { argvOpt = Some(argv) } /** @return the actual Scala CLI program name which was run */ protected def progName: String = ScalaCli.progName /** @return the actual Scala CLI runner name which was run */ protected def fullRunnerName = ScalaCli.fullRunnerName /** @return the actual Scala CLI base runner name, for SIP it is scala otherwise scala-cli */ protected def baseRunnerName = ScalaCli.baseRunnerName // TODO Manage to have case-app give use the exact command name that was used instead /** The actual sub-command name that was used. If the sub-command name is a list of strings, space * is used as the separator. If [[argvOpt]] hasn't been defined, it defaults to [[name]]. */ protected def actualCommandName: String = argvOpt.map { argv => @tailrec def validCommand(potentialCommandName: List[String]): Option[List[String]] = if potentialCommandName.isEmpty then None else names.find(_ == potentialCommandName) match { case cmd @ Some(_) => cmd case _ => validCommand(potentialCommandName.dropRight(1)) } val maxCommandLength: Int = names.map(_.length).max max 1 val maxPotentialCommandNames = argv.slice(1, maxCommandLength + 1).toList validCommand(maxPotentialCommandNames).getOrElse(List("")) }.getOrElse(List(inHelp.progName)).mkString(" ") protected def actualFullCommand: String = if actualCommandName.nonEmpty then s"$progName $actualCommandName" else progName protected def invokeData: ScalaCliInvokeData = ScalaCliInvokeData( progName, actualCommandName, SubCommand.Other, ProcUtil.isShebangCapableShell ) given ScalaCliInvokeData = invokeData override def error(message: Error): Nothing = { System.err.println( s"""${message.message} | |To list all available options, run | ${Console.BOLD}$actualFullCommand --help${Console.RESET}""".stripMargin ) sys.exit(1) } // FIXME Report this in case-app default NameFormatter override lazy val nameFormatter: Formatter[Name] = { val parent = super.nameFormatter (t: Name) => if (t.name.startsWith("-")) t.name else parent.format(t) } override def completer: Completer[T] = { val parent = super.completer new Completer[T] { def optionName(prefix: String, state: Option[T], args: RemainingArgs): List[CompletionItem] = parent.optionName(prefix, state, args) def optionValue( arg: Arg, prefix: String, state: Option[T], args: RemainingArgs ): List[CompletionItem] = { val candidates = arg.name.name match { case "dependency" => state.flatMap(sharedOptions).toList.flatMap { sharedOptions => val logger = sharedOptions.logger val cache = sharedOptions.coursierCache val sv = sharedOptions.buildOptions().orExit(logger) .scalaParams .toOption .flatten .map(_.scalaVersion) .getOrElse(defaultScalaVersion) val (fromIndex, completions) = cache.logger.use { coursier.complete.Complete(cache) .withInput(prefix) .withScalaVersion(sv) .complete() .unsafeRun()(using cache.ec) } if (completions.isEmpty) Nil else { val prefix0 = prefix.take(fromIndex) val values = completions.map(c => prefix0 + c) values.map { str => CompletionItem(str) } } } case "repository" => Nil // TODO case _ => Nil } candidates ++ parent.optionValue(arg, prefix, state, args) } def argument(prefix: String, state: Option[T], args: RemainingArgs): List[CompletionItem] = parent.argument(prefix, state, args) } } def maybePrintGroupHelp(options: T): Unit = for (shared <- sharedOptions(options)) shared.helpGroups.maybePrintGroupHelp(help, helpFormat) private def maybePrintWarnings(options: T): Unit = { import scala.cli.commands.shared.ScalacOptions.YScriptRunnerOption val logger = options.global.logging.logger sharedOptions(options).foreach { so => val scalacOpts = so.scalacOptions.toScalacOptShadowingSeq scalacOpts.keys .find(_.value.noDashPrefixes == YScriptRunnerOption) .map(_.value) .foreach(k => logger.message(LegacyScalaOptions.yScriptRunnerWarning(k, scalacOpts.getOption(k))) ) } } /** Print `scalac` output if passed options imply no inputs are necessary and raw `scalac` output * is required instead. (i.e. `--scalac-option -help`) * @param options * command options */ def maybePrintSimpleScalacOutput(options: T, buildOptions: BuildOptions): Unit = for { shared <- sharedOptions(options) scalacOptions = shared.scalacOptions updatedScalacOptions = scalacOptions.withScalacExtraOptions(shared.scalacExtra) if updatedScalacOptions.map(_.noDashPrefixes).exists(ScalacOptions.isScalacPrintOption) logger = shared.logger fixedBuildOptions = buildOptions.copy(scalaOptions = buildOptions.scalaOptions.copy(defaultScalaVersion = Some(ScalaCli.getDefaultScalaVersion)) ) artifacts <- fixedBuildOptions.artifacts(logger, Scope.Main).toOption scalaArtifacts <- artifacts.scalaOpt compilerClassPath = scalaArtifacts.compilerClassPath scalaVersion = scalaArtifacts.params.scalaVersion compileClassPath = artifacts.compileClassPath simpleScalaCompiler = SimpleScalaCompiler("java", Nil, scaladoc = false) javacOptions = fixedBuildOptions.javaOptions.javacOptions.map(_.value) javaHome = fixedBuildOptions.javaHomeLocation().value } { val exitCode = simpleScalaCompiler.runSimpleScalacLike( scalaVersion, Option(javaHome), javacOptions, updatedScalacOptions, compileClassPath, compilerClassPath, logger ) sys.exit(exitCode) } def maybePrintToolsHelp(options: T, buildOptions: BuildOptions): Unit = for { shared <- sharedOptions(options) if shared.helpGroups.helpScaladoc || shared.helpGroups.helpRepl || shared.helpGroups.helpScalafmt logger = shared.logger artifacts <- buildOptions.artifacts(logger, Scope.Main).toOption scalaArtifacts <- artifacts.scalaOpt scalaParams = scalaArtifacts.params } { val exitCode: Either[BuildException, Int] = either { val (classPath: Seq[os.Path], mainClass: String) = if (shared.helpGroups.helpScaladoc) { val docArtifacts = value { Artifacts.fetchAnyDependencies( Seq(Positioned.none(dep"org.scala-lang::scaladoc:${scalaParams.scalaVersion}")), value(buildOptions.finalRepositories), Some(scalaParams), logger, buildOptions.finalCache, None ) } docArtifacts.files.map(os.Path(_, os.pwd)) -> "dotty.tools.scaladoc.Main" } else if (shared.helpGroups.helpRepl) { val initialBuildOptions = buildOptionsOrExit(options) val artifacts = initialBuildOptions.artifacts(logger, Scope.Main).orExit(logger) val javaVersion: Int = initialBuildOptions.javaHome().value.version val replArtifacts = value { ReplArtifacts.default( scalaParams = scalaParams, dependencies = artifacts.userDependencies, extraClassPath = Nil, logger = logger, cache = buildOptions.finalCache, repositories = Nil, addScalapy = None, javaVersion = javaVersion ) } replArtifacts.replClassPath -> replArtifacts.replMainClass } else { val fmtArtifacts = value { Artifacts.fetchAnyDependencies( Seq(Positioned.none( dep"${Constants.scalafmtOrganization}:${Constants.scalafmtName}:${Constants.defaultScalafmtVersion}" )), value(buildOptions.finalRepositories), Some(scalaParams), logger, buildOptions.finalCache, None ) } fmtArtifacts.files.map(os.Path(_, os.pwd)) -> "org.scalafmt.cli.Cli" } val retCode = Runner.runJvm( buildOptions.javaHome().value.javaCommand, Nil, classPath, mainClass, Seq("-help"), logger ).waitFor() retCode } sys.exit(exitCode.orExit(logger)) } private def maybePrintEnvsHelp(options: T): Unit = if sharedOptions(options).exists(_.helpGroups.helpEnvs) then println(EnvVar.helpMessage(isPower = allowRestrictedFeatures)) sys.exit(0) override def helpFormat: HelpFormat = ScalaCliHelp.helpFormat override val messages: Help[T] = if shouldExcludeInSip then inHelp.copy(helpMessage = Some(HelpMessage(WarningMessages.powerCommandUsedInSip( actualCommandName, scalaSpecificationLevel ))) ) else if isExperimental then inHelp.copy(helpMessage = inHelp.helpMessage.map(hm => hm.copy( message = s"""${hm.message} | |${WarningMessages.experimentalSubcommandWarning(inHelp.progName)}""".stripMargin, detailedMessage = if hm.detailedMessage.nonEmpty then s"""${hm.detailedMessage} | |${WarningMessages.experimentalSubcommandWarning(inHelp.progName)}""".stripMargin else hm.detailedMessage ) ) ) else inHelp /** @param options * command-specific [[T]] options * @return * Tries to create BuildOptions based on [[sharedOptions]] and exits on error. Override to * change this behaviour. */ def buildOptions(options: T): Option[BuildOptions] = sharedOptions(options) .map(shared => shared.buildOptions().orExit(shared.logger)) protected def buildOptionsOrExit(options: T): BuildOptions = buildOptions(options) .map(bo => bo.copy(scalaOptions = bo.scalaOptions.copy(defaultScalaVersion = Some(defaultScalaVersion)) ) ) .getOrElse { sharedOptions(options).foreach(_.logger.debug("build options could not be initialized")) sys.exit(1) } override def shouldSuppressExperimentalFeatureWarnings: Boolean = globalOptions.globalSuppress.suppressExperimentalFeatureWarning .orElse { configDb.toOption .flatMap(_.getOpt(Keys.suppressExperimentalFeatureWarning)) } .getOrElse(false) override def shouldSuppressDeprecatedFeatureWarnings: Boolean = globalOptions.globalSuppress.suppressDeprecatedFeatureWarning .orElse { configDb.toOption .flatMap(_.getOpt(Keys.suppressDeprecatedFeatureWarning)) } .getOrElse(false) override def logger: Logger = globalOptions.logging.logger final override def main(progName: String, args: Array[String]): Unit = { globalOptionsAtomic.set(GlobalOptions.get(args.toList).getOrElse(GlobalOptions.default)) super.main(progName, args) } /** This should be overridden instead of [[run]] when extending [[ScalaCommand]]. * * @param options * the command's specific set of options * @param remainingArgs * arguments remaining after parsing options */ def runCommand(options: T, remainingArgs: RemainingArgs, logger: Logger): Unit /** This implementation is final. Override [[runCommand]] instead. This logic is invoked at the * start of running every [[ScalaCommand]]. */ final override def run(options: T, remainingArgs: RemainingArgs): Unit = { CurrentParams.verbosity = options.global.logging.verbosity if shouldExcludeInSip then logger.error(WarningMessages.powerCommandUsedInSip( actualCommandName, scalaSpecificationLevel )) sys.exit(1) else if isExperimental && !shouldSuppressExperimentalFeatureWarnings then logger.experimentalWarning(name, FeatureType.Subcommand) maybePrintWarnings(options) maybePrintGroupHelp(options) buildOptions(options).foreach { bo => maybePrintSimpleScalacOutput(options, bo) maybePrintToolsHelp(options, bo) } maybePrintEnvsHelp(options) logger.flushExperimentalWarnings runCommand(options, remainingArgs, options.global.logging.logger) } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/ScalaCommandWithCustomHelp.scala ================================================ package scala.cli.commands import caseapp.core.Error import caseapp.core.help.{Help, HelpCompanion, RuntimeCommandsHelp} import caseapp.core.parser.Parser import scala.cli.commands.default.LegacyScalaOptions import scala.cli.commands.shared.{AllExternalHelpOptions, HasGlobalOptions} import scala.cli.commands.util.HelpUtils.* import scala.cli.launcher.LauncherOptions abstract class ScalaCommandWithCustomHelp[T <: HasGlobalOptions]( actualHelp: => RuntimeCommandsHelp )( implicit myParser: Parser[T], help: Help[T] ) extends ScalaCommand[T] { private def launcherHelp: Help[LauncherOptions] = HelpCompanion.deriveHelp[LauncherOptions] private def legacyScalaHelp: Help[LegacyScalaOptions] = HelpCompanion.deriveHelp[LegacyScalaOptions] protected def customHelp(showHidden: Boolean): String = { val helpString = actualHelp.help(helpFormat, showHidden) val launcherHelpString = launcherHelp.optionsHelp(helpFormat, showHidden) val legacyScalaHelpString = legacyScalaHelp.optionsHelp(helpFormat, showHidden) val allExternalHelp = HelpCompanion.deriveHelp[AllExternalHelpOptions] val allExternalHelpString = allExternalHelp.optionsHelp(helpFormat, showHidden) val legacyScalaHelpStringWithPadding = if legacyScalaHelpString.nonEmpty then s""" |$legacyScalaHelpString |""".stripMargin else "" s"""$helpString | |$launcherHelpString | |$allExternalHelpString |$legacyScalaHelpStringWithPadding""".stripMargin } protected def customHelpAsked(showHidden: Boolean): Nothing = { println(customHelp(showHidden)) sys.exit(0) } override def helpAsked(progName: String, maybeOptions: Either[Error, T]): Nothing = customHelpAsked(showHidden = false) override def fullHelpAsked(progName: String): Nothing = customHelpAsked(showHidden = true) } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/ScalaVersions.scala ================================================ package scala.cli.commands final case class ScalaVersions( version: String, binaryVersion: String ) ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/WatchUtil.scala ================================================ package scala.cli.commands import scala.annotation.tailrec import scala.build.internal.StdInConcurrentReader import scala.build.internals.ConsoleUtils.ScalaCliConsole object WatchUtil { lazy val isDevMode: Boolean = Option(getClass.getProtectionDomain.getCodeSource) .exists(_.getLocation.toExternalForm.endsWith("classes/")) def waitMessage(message: String): String = { // Both short cuts actually always work, but Ctrl+C also exits mill in mill watch mode. val shortCut = if (isDevMode) "Ctrl+D" else "Ctrl+C" gray(s"$message, press $shortCut to exit, or press Enter to re-run.") } private def gray(message: String): String = { val gray = ScalaCliConsole.GRAY val reset = Console.RESET s"$gray$message$reset" } def printWatchMessage(): Unit = System.err.println(waitMessage("Watching sources")) def printWatchWhileRunningMessage(): Unit = System.err.println(gray("Watching sources while your program is running.")) def waitForCtrlC( onPressEnter: () => Unit = () => (), shouldReadInput: () => Boolean = () => true ): Unit = synchronized { @tailrec def readNextChar(): Int = if (shouldReadInput()) try StdInConcurrentReader.waitForLine().map(s => (s + '\n').head.toInt).getOrElse(-1) catch { case _: InterruptedException => // Actually never called, as System.in.read isn't interruptible… // That means we sometimes read input when we shouldn't. readNextChar() } else { try wait() catch { case _: InterruptedException => } readNextChar() } var readKey = -1 while ({ readKey = readNextChar() readKey != -1 }) if (readKey == '\n') onPressEnter() } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/addpath/AddPath.scala ================================================ package scala.cli.commands.addpath import caseapp.* import coursier.env.{EnvironmentUpdate, ProfileUpdater} import java.io.File import scala.build.Logger import scala.build.internals.EnvVar import scala.cli.commands.{CustomWindowsEnvVarUpdater, ScalaCommand} import scala.util.Properties object AddPath extends ScalaCommand[AddPathOptions] { override def hidden = true override def scalaSpecificationLevel = SpecificationLevel.RESTRICTED override def runCommand(options: AddPathOptions, args: RemainingArgs, logger: Logger): Unit = { if args.all.isEmpty then logger.error("Nothing to do") else { val update = EnvironmentUpdate( Nil, Seq(EnvVar.Misc.path.name -> args.all.mkString(File.pathSeparator)) ) val didUpdate = if (Properties.isWin) { val updater = CustomWindowsEnvVarUpdater().withUseJni(Some(coursier.paths.Util.useJni())) updater.applyUpdate(update) } else { val updater = ProfileUpdater() updater.applyUpdate(update, Some(options.title).filter(_.nonEmpty)) } if !didUpdate then logger.log("Everything up-to-date") } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/addpath/AddPathOptions.scala ================================================ package scala.cli.commands.addpath import caseapp.* import scala.cli.commands.shared.{GlobalOptions, HasGlobalOptions} import scala.cli.commands.tags // format: off @HelpMessage("Add entries to the PATH environment variable.") final case class AddPathOptions( @Recurse global: GlobalOptions = GlobalOptions(), @Tag(tags.restricted) title: String = "" ) extends HasGlobalOptions // format: on object AddPathOptions { implicit lazy val parser: Parser[AddPathOptions] = Parser.derive implicit lazy val help: Help[AddPathOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/bloop/Bloop.scala ================================================ package scala.cli.commands.bloop import bloop.rifle.internal.Operations import bloop.rifle.{BloopRifle, BloopRifleConfig, BloopThreads} import caseapp.core.RemainingArgs import scala.build.internal.Constants import scala.build.{Directories, Logger} import scala.cli.commands.ScalaCommand import scala.cli.commands.shared.SharedOptions import scala.cli.commands.util.JvmUtils import scala.concurrent.Await import scala.concurrent.duration.Duration object Bloop extends ScalaCommand[BloopOptions] { override def hidden = true override def scalaSpecificationLevel = SpecificationLevel.RESTRICTED override def stopAtFirstUnrecognized = true private def bloopRifleConfig0(opts: BloopOptions): BloopRifleConfig = { // FIXME Basically a tweaked copy of SharedOptions.bloopRifleConfig // Some in progress BuildOptions / JavaOptions refactoring of mine should allow // to stop using SharedOptions and BuildOptions here, and deal with JavaOptions // directly. val sharedOptions = SharedOptions( logging = opts.global.logging, compilationServer = opts.compilationServer, jvm = opts.jvm, coursier = opts.coursier ) val options = sharedOptions.buildOptions().orExit(opts.global.logging.logger) val javaHomeInfo = opts.compilationServer.bloopJvm .map(JvmUtils.downloadJvm(_, options)) .getOrElse { JvmUtils.getJavaCmdVersionOrHigher(Constants.minimumBloopJavaVersion, options) }.orExit(logger) opts.compilationServer.bloopRifleConfig( opts.global.logging.logger, sharedOptions.coursierCache, opts.global.logging.verbosity, javaHomeInfo.javaCommand, Directories.directories, Some(javaHomeInfo.version) ) } override def runCommand(options: BloopOptions, args: RemainingArgs, logger: Logger): Unit = { val threads = BloopThreads.create() val bloopRifleConfig = bloopRifleConfig0(options) val isRunning = BloopRifle.check(bloopRifleConfig, logger.bloopRifleLogger) if (isRunning) logger.debug("Found running Bloop server") else { logger.debug("No running Bloop server found, starting one") val f = BloopRifle.startServer( bloopRifleConfig, threads.startServerChecks, logger.bloopRifleLogger, bloopRifleConfig.retainedBloopVersion.version.raw, bloopRifleConfig.javaPath ) Await.result(f, Duration.Inf) logger.message("Bloop server started.") } val args0 = args.all args0 match { case Seq() => // FIXME Give more details? logger.message("Bloop server is running.") case Seq(cmd, args @ _*) => val assumeTty = System.console() != null val workingDir = options.workDirOpt.getOrElse(os.pwd).toNIO val retCode = Operations.run( command = cmd, args = args.toArray, workingDir = workingDir, address = bloopRifleConfig.address, inOpt = Some(System.in), out = System.out, err = System.err, logger = logger.bloopRifleLogger, assumeInTty = assumeTty, assumeOutTty = assumeTty, assumeErrTty = assumeTty ) if (retCode == 0) logger.debug(s"Bloop command $cmd ran successfully (return code 0)") else { logger.debug(s"Got return code $retCode from Bloop server when running $cmd, exiting with it") sys.exit(retCode) } } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/bloop/BloopExit.scala ================================================ package scala.cli.commands.bloop import bloop.rifle.{BloopRifle, BloopRifleConfig} import caseapp.* import scala.build.{Directories, Logger, Os} import scala.cli.commands.ScalaCommand object BloopExit extends ScalaCommand[BloopExitOptions] { override def hidden = true override def scalaSpecificationLevel = SpecificationLevel.RESTRICTED override def names: List[List[String]] = List( List("bloop", "exit") ) private def mkBloopRifleConfig(opts: BloopExitOptions): BloopRifleConfig = { import opts.* compilationServer.bloopRifleConfig( global.logging.logger, coursier.coursierCache(global.logging.logger, cacheLoggerPrefix = "Downloading Bloop"), global.logging.verbosity, "java", // shouldn't be used… Directories.directories ) } override def runCommand(options: BloopExitOptions, args: RemainingArgs, logger: Logger): Unit = { val bloopRifleConfig = mkBloopRifleConfig(options) val isRunning = BloopRifle.check(bloopRifleConfig, logger.bloopRifleLogger) if (isRunning) { val ret = BloopRifle.exit(bloopRifleConfig, Os.pwd.toNIO, logger.bloopRifleLogger) logger.debug(s"Bloop exit returned code $ret") if (ret == 0) logger.message("Stopped Bloop server.") else { if (options.global.logging.verbosity >= 0) System.err.println(s"Error running bloop exit command (return code $ret)") sys.exit(1) } } else logger.message("No running Bloop server found.") } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/bloop/BloopExitOptions.scala ================================================ package scala.cli.commands.bloop import caseapp.* import scala.cli.commands.shared.{CoursierOptions, GlobalOptions, HasGlobalOptions, HelpMessages, SharedCompilationServerOptions} // format: off @HelpMessage( s"""Stop Bloop if an instance is running. | |${HelpMessages.bloopInfo}""".stripMargin) final case class BloopExitOptions( @Recurse global: GlobalOptions = GlobalOptions(), @Recurse compilationServer: SharedCompilationServerOptions = SharedCompilationServerOptions(), @Recurse coursier: CoursierOptions = CoursierOptions() ) extends HasGlobalOptions // format: on object BloopExitOptions { implicit lazy val parser: Parser[BloopExitOptions] = Parser.derive implicit lazy val help: Help[BloopExitOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/bloop/BloopJson.scala ================================================ package scala.cli.commands.bloop import com.github.plokhotnyuk.jsoniter_scala.core.* import com.github.plokhotnyuk.jsoniter_scala.macros.* final case class BloopJson(javaOptions: List[String] = Nil) object BloopJson { val codec: JsonValueCodec[BloopJson] = JsonCodecMaker.make } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/bloop/BloopOptions.scala ================================================ package scala.cli.commands.bloop import caseapp.* import scala.cli.commands.shared.* import scala.cli.commands.tags // format: off @HelpMessage(BloopOptions.helpMessage, "", BloopOptions.detailedHelpMessage) final case class BloopOptions( @Recurse global: GlobalOptions = GlobalOptions(), @Recurse compilationServer: SharedCompilationServerOptions = SharedCompilationServerOptions(), @Recurse jvm: SharedJvmOptions = SharedJvmOptions(), @Recurse coursier: CoursierOptions = CoursierOptions(), @ExtraName("workingDir") @ExtraName("dir") @Tag(tags.restricted) workingDirectory: Option[String] = None ) extends HasGlobalOptions { // format: on def workDirOpt: Option[os.Path] = workingDirectory .filter(_.trim.nonEmpty) .map(os.Path(_, os.pwd)) } object BloopOptions { implicit lazy val parser: Parser[BloopOptions] = Parser.derive implicit lazy val help: Help[BloopOptions] = Help.derive val helpMessage: String = "Interact with Bloop (the build server) or check its status." val detailedHelpMessage: String = s"""$helpMessage | |This sub-command allows to check the current status of Bloop. |If Bloop isn't currently running, it will be started. | |${HelpMessages.bloopInfo}""".stripMargin } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/bloop/BloopOutput.scala ================================================ package scala.cli.commands.bloop import bloop.rifle.BloopRifleConfig import caseapp.core.RemainingArgs import scala.build.{Directories, Logger} import scala.cli.commands.ScalaCommand import scala.cli.commands.shared.CoursierOptions object BloopOutput extends ScalaCommand[BloopOutputOptions] { override def hidden = true override def scalaSpecificationLevel = SpecificationLevel.RESTRICTED override def names: List[List[String]] = List( List("bloop", "output") ) override def runCommand(options: BloopOutputOptions, args: RemainingArgs, logger: Logger): Unit = { val bloopRifleConfig = options.compilationServer.bloopRifleConfig( logger, CoursierOptions().coursierCache(logger, cacheLoggerPrefix = "Downloading Bloop"), // unused here options.global.logging.verbosity, "unused-java", // unused here Directories.directories ) val outputFile = bloopRifleConfig.address match { case s: BloopRifleConfig.Address.DomainSocket => logger.debug(s"Bloop server directory: ${s.path}") logger.debug(s"Bloop server output path: ${s.outputPath}") os.Path(s.outputPath, os.pwd) case tcp: BloopRifleConfig.Address.Tcp => if (options.global.logging.verbosity >= 0) System.err.println( s"Error: Bloop server is listening on TCP at ${tcp.render}, output not available." ) sys.exit(1) } if (!os.isFile(outputFile)) { if (options.global.logging.verbosity >= 0) System.err.println(s"Error: $outputFile not found") sys.exit(1) } val content = os.read.bytes(outputFile) logger.debug(s"Read ${content.length} bytes from $outputFile") System.out.write(content) } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/bloop/BloopOutputOptions.scala ================================================ package scala.cli.commands.bloop import caseapp.* import scala.cli.commands.shared.{GlobalOptions, HasGlobalOptions, HelpMessages, SharedCompilationServerOptions} // format: off @HelpMessage( s"""Print Bloop output. | |${HelpMessages.bloopInfo}""".stripMargin) final case class BloopOutputOptions( @Recurse global: GlobalOptions = GlobalOptions(), @Recurse compilationServer: SharedCompilationServerOptions = SharedCompilationServerOptions(), ) extends HasGlobalOptions // format: on object BloopOutputOptions { implicit lazy val parser: Parser[BloopOutputOptions] = Parser.derive implicit lazy val help: Help[BloopOutputOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/bloop/BloopStart.scala ================================================ package scala.cli.commands.bloop import bloop.rifle.internal.BuildInfo import bloop.rifle.{BloopRifle, BloopRifleConfig, BloopThreads} import caseapp.* import scala.build.options.{BuildOptions, InternalOptions} import scala.build.{Directories, Logger, Os} import scala.cli.commands.ScalaCommand import scala.cli.commands.util.JvmUtils import scala.concurrent.Await import scala.concurrent.duration.Duration object BloopStart extends ScalaCommand[BloopStartOptions] { override def hidden = true override def scalaSpecificationLevel = SpecificationLevel.RESTRICTED override def names: List[List[String]] = List( List("bloop", "start") ) private def mkBloopRifleConfig(opts: BloopStartOptions): BloopRifleConfig = { import opts.* val buildOptions = BuildOptions( javaOptions = JvmUtils.javaOptions(jvm).orExit(global.logging.logger), internal = InternalOptions(cache = Some(coursier.coursierCache(global.logging.logger))) ) compilationServer.bloopRifleConfig( global.logging.logger, coursier.coursierCache(global.logging.logger, cacheLoggerPrefix = "Downloading Bloop"), global.logging.verbosity, buildOptions.javaHome().value.javaCommand, Directories.directories ) } override def runCommand(options: BloopStartOptions, args: RemainingArgs, logger: Logger): Unit = { val threads = BloopThreads.create() val bloopRifleConfig = mkBloopRifleConfig(options) val isRunning = BloopRifle.check(bloopRifleConfig, logger.bloopRifleLogger) if (isRunning && options.force) { logger.message("Found Bloop server running, stopping it.") val ret = BloopRifle.exit(bloopRifleConfig, Os.pwd.toNIO, logger.bloopRifleLogger) logger.debug(s"Bloop exit returned code $ret") if (ret == 0) logger.message("Stopped Bloop server.") else { if (options.global.logging.verbosity >= 0) System.err.println(s"Error running bloop exit command (return code $ret)") sys.exit(1) } } if (isRunning && !options.force) logger.message("Bloop server already running.") else { val f = BloopRifle.startServer( bloopRifleConfig, threads.startServerChecks, logger.bloopRifleLogger, BuildInfo.version, bloopRifleConfig.javaPath ) Await.result(f, Duration.Inf) logger.message("Bloop server started.") } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/bloop/BloopStartOptions.scala ================================================ package scala.cli.commands.bloop import caseapp.* import scala.cli.commands.shared.* import scala.cli.commands.tags // format: off @HelpMessage( s"""Starts a Bloop instance, if none is running. | |${HelpMessages.bloopInfo}""".stripMargin) final case class BloopStartOptions( @Recurse global: GlobalOptions = GlobalOptions(), @Recurse compilationServer: SharedCompilationServerOptions = SharedCompilationServerOptions(), @Recurse jvm: SharedJvmOptions = SharedJvmOptions(), @Recurse coursier: CoursierOptions = CoursierOptions(), @Name("f") @Tag(tags.restricted) force: Boolean = false ) extends HasGlobalOptions // format: on object BloopStartOptions { implicit lazy val parser: Parser[BloopStartOptions] = Parser.derive implicit lazy val help: Help[BloopStartOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/bsp/Bsp.scala ================================================ package scala.cli.commands.bsp import caseapp.* import com.github.plokhotnyuk.jsoniter_scala.core.* import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker import scala.build.* import scala.build.EitherCps.{either, value} import scala.build.bsp.{BspReloadableOptions, BspThreads} import scala.build.errors.BuildException import scala.build.input.Inputs import scala.build.internals.EnvVar import scala.build.options.{BuildOptions, Scope} import scala.cli.commands.ScalaCommand import scala.cli.commands.shared.SharedOptions import scala.cli.config.Keys import scala.cli.launcher.LauncherOptions import scala.cli.util.ConfigDbUtils import scala.cli.{CurrentParams, ScalaCli} import scala.concurrent.Await import scala.concurrent.duration.Duration object Bsp extends ScalaCommand[BspOptions] { override def hidden = true override def scalaSpecificationLevel = SpecificationLevel.IMPLEMENTATION private def latestSharedOptions(options: BspOptions): SharedOptions = options.jsonOptions .map(path => os.Path(path, os.pwd)) .filter(path => os.exists(path) && os.isFile(path)) .map { optionsPath => val content = os.read.bytes(os.Path(optionsPath, os.pwd)) readFromArray(content)(using SharedOptions.jsonCodec) }.getOrElse(options.shared) private def latestLauncherOptions(options: BspOptions): LauncherOptions = options.jsonLauncherOptions .map(path => os.Path(path, os.pwd)) .filter(path => os.exists(path) && os.isFile(path)) .map { optionsPath => val content = os.read.bytes(os.Path(optionsPath, os.pwd)) readFromArray(content)(using LauncherOptions.jsonCodec) }.getOrElse(launcherOptions) private def latestEnvsFromFile(options: BspOptions): Map[String, String] = options.envs .map(path => os.Path(path, os.pwd)) .filter(path => os.exists(path) && os.isFile(path)) .map { envsPath => val content = os.read.bytes(os.Path(envsPath, os.pwd)) implicit val mapCodec: JsonValueCodec[Map[String, String]] = JsonCodecMaker.make readFromArray(content) } .getOrElse(Map.empty) override def sharedOptions(options: BspOptions): Option[SharedOptions] = Option(latestSharedOptions(options)) private def refreshPowerMode( latestLauncherOptions: LauncherOptions, latestSharedOptions: SharedOptions, latestEnvs: Map[String, String] ): Unit = { val previousPowerMode = ScalaCli.allowRestrictedFeatures val configPowerMode = ConfigDbUtils.getLatestConfigDbOpt(latestSharedOptions.logger) .flatMap(_.get(Keys.power).toOption) .flatten .getOrElse(false) val envPowerMode = latestEnvs.get(EnvVar.ScalaCli.power.name).exists(_.toBoolean) val launcherPowerArg = latestLauncherOptions.powerOptions.power val subCommandPowerArg = latestSharedOptions.powerOptions.power val latestPowerMode = configPowerMode || launcherPowerArg || subCommandPowerArg || envPowerMode // only set power mode if it's been turned on since, never turn it off in BSP if !previousPowerMode && latestPowerMode then ScalaCli.setPowerMode(latestPowerMode) } // not reusing buildOptions here, since they should be reloaded live instead override def runCommand(options: BspOptions, args: RemainingArgs, logger: Logger): Unit = { if (options.shared.logging.verbosity >= 3) pprint.err.log(args) val getSharedOptions: () => SharedOptions = () => latestSharedOptions(options) val getLauncherOptions: () => LauncherOptions = () => latestLauncherOptions(options) val getEnvsFromFile: () => Map[String, String] = () => latestEnvsFromFile(options) refreshPowerMode(getLauncherOptions(), getSharedOptions(), getEnvsFromFile()) val preprocessInputs: Seq[String] => Either[BuildException, (Inputs, BuildOptions)] = argsSeq => either { val sharedOptions = getSharedOptions() val launcherOptions = getLauncherOptions() val envs = getEnvsFromFile() val initialInputs = value(sharedOptions.inputs(argsSeq, () => Inputs.default())) refreshPowerMode(launcherOptions, sharedOptions, envs) if (sharedOptions.logging.verbosity >= 3) pprint.err.log(initialInputs) val baseOptions = buildOptions(sharedOptions, launcherOptions, envs) val latestLogger = sharedOptions.logging.logger val persistentLogger = new PersistentDiagnosticLogger(latestLogger) val crossResult = CrossSources.forInputs( initialInputs, Sources.defaultPreprocessors( baseOptions.archiveCache, baseOptions.internal.javaClassNameVersionOpt, () => baseOptions.javaHome().value.javaCommand ), persistentLogger, baseOptions.suppressWarningOptions, baseOptions.internal.exclude, download = baseOptions.downloader ) val (allInputs, finalBuildOptions) = { for crossSourcesAndInputs <- crossResult // compiler bug, can't do : // (crossSources, crossInputs) <- crossResult (crossSources, crossInputs) = crossSourcesAndInputs sharedBuildOptions = crossSources.sharedOptions(baseOptions) scopedSources <- crossSources.scopedSources(sharedBuildOptions) resolvedBuildOptions = scopedSources.buildOptionsFor(Scope.Main).foldRight(sharedBuildOptions)(_.orElse(_)) yield (crossInputs, resolvedBuildOptions) }.getOrElse(initialInputs -> baseOptions) Build.updateInputs(allInputs, baseOptions) -> finalBuildOptions } val (inputs, finalBuildOptions) = preprocessInputs(args.all).orExit(logger) /** values used for launching the bsp, especially for launching the bloop server, they do not * include options extracted from sources, except in bloopRifleConfig - it's needed for * correctly launching the bloop server */ val initialBspOptions = { val sharedOptions = getSharedOptions() val launcherOptions = getLauncherOptions() val envs = getEnvsFromFile() val bspBuildOptions = buildOptions(sharedOptions, launcherOptions, envs) refreshPowerMode(launcherOptions, sharedOptions, envs) BspReloadableOptions( buildOptions = bspBuildOptions, bloopRifleConfig = sharedOptions.bloopRifleConfig(Some(finalBuildOptions)) .orExit(sharedOptions.logger), logger = sharedOptions.logging.logger, verbosity = sharedOptions.logging.verbosity ) } val bspReloadableOptionsReference = BspReloadableOptions.Reference { () => val sharedOptions = getSharedOptions() val launcherOptions = getLauncherOptions() val envs = getEnvsFromFile() refreshPowerMode(launcherOptions, sharedOptions, envs) BspReloadableOptions( buildOptions = buildOptions(sharedOptions, launcherOptions, envs), bloopRifleConfig = sharedOptions.bloopRifleConfig().orExit(sharedOptions.logger), logger = sharedOptions.logging.logger, verbosity = sharedOptions.logging.verbosity ) } CurrentParams.workspaceOpt = Some(inputs.workspace) val actionableDiagnostics = options.shared.logging.verbosityOptions.actions BspThreads.withThreads { threads => val bsp = scala.build.bsp.Bsp.create( preprocessInputs.andThen(_.map(_._1)), bspReloadableOptionsReference, threads, System.in, System.out, actionableDiagnostics ) try { val doneFuture = bsp.run(inputs, initialBspOptions) Await.result(doneFuture, Duration.Inf) } finally bsp.shutdown() } } private def buildOptions( sharedOptions: SharedOptions, launcherOptions: LauncherOptions, envs: Map[String, String] ): BuildOptions = { val logger = sharedOptions.logger val baseOptions: BuildOptions = sharedOptions.buildOptions().orExit(logger) val withDefaults: BuildOptions = baseOptions.copy( classPathOptions = baseOptions.classPathOptions.copy( fetchSources = baseOptions.classPathOptions.fetchSources.orElse(Some(true)) ), scalaOptions = baseOptions.scalaOptions.copy( semanticDbOptions = baseOptions.scalaOptions.semanticDbOptions.copy( generateSemanticDbs = baseOptions.scalaOptions.semanticDbOptions.generateSemanticDbs.orElse(Some(true)) ) ), notForBloopOptions = baseOptions.notForBloopOptions.copy( addRunnerDependencyOpt = baseOptions.notForBloopOptions.addRunnerDependencyOpt.orElse(Some(false)) ) ) val withEnvs: BuildOptions = envs.get(EnvVar.Java.javaHome.name) .filter(_ => withDefaults.javaOptions.javaHomeOpt.isEmpty) .map(javaHome => withDefaults.copy(javaOptions = withDefaults.javaOptions.copy(javaHomeOpt = Some(Positioned( Seq(Position.Custom("ide.env.JAVA_HOME")), os.Path(javaHome, Os.pwd) )) ) ) ) .getOrElse(withDefaults) val withLauncherOptions = withEnvs.copy( classPathOptions = withEnvs.classPathOptions.copy( extraRepositories = (withEnvs.classPathOptions.extraRepositories ++ launcherOptions.scalaRunner .cliPredefinedRepository).distinct ), scalaOptions = withEnvs.scalaOptions.copy( defaultScalaVersion = launcherOptions.scalaRunner.cliUserScalaVersion ) ) withLauncherOptions } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/bsp/BspOptions.scala ================================================ package scala.cli.commands.bsp import caseapp.* import scala.cli.ScalaCli.fullRunnerName import scala.cli.commands.shared.{HasSharedOptions, HelpMessages, SharedOptions} import scala.cli.commands.tags // format: off @HelpMessage(BspOptions.helpMessage, "", BspOptions.detailedHelpMessage) final case class BspOptions( // FIXME There might be too many options in SharedOptions for the bsp command… @Recurse shared: SharedOptions = SharedOptions(), @HelpMessage("Command-line options JSON file") @ValueDescription("path") @Hidden @Tag(tags.implementation) jsonOptions: Option[String] = None, @HelpMessage("Command-line launcher options JSON file") @ValueDescription("path") @Hidden @Tag(tags.implementation) jsonLauncherOptions: Option[String] = None, @HelpMessage("Command-line options environment variables file") @ValueDescription("path") @Hidden @Tag(tags.implementation) @Name("envsFile") envs: Option[String] = None ) extends HasSharedOptions { // format: on } object BspOptions { implicit lazy val parser: Parser[BspOptions] = Parser.derive implicit lazy val help: Help[BspOptions] = Help.derive val cmdName = "bsp" private val helpHeader = "Start BSP server." val helpMessage: String = s"""$helpHeader | |${HelpMessages.commandFullHelpReference(cmdName)} |${HelpMessages.docsWebsiteReference}""".stripMargin val detailedHelpMessage: String = s"""$helpHeader | |BSP stands for Build Server Protocol. |For more information refer to https://build-server-protocol.github.io/ | |This sub-command is not designed to be used by a human. |It is normally supposed to be invoked by your IDE when a $fullRunnerName project is imported. | |${HelpMessages.docsWebsiteReference}""".stripMargin } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/clean/Clean.scala ================================================ package scala.cli.commands.clean import caseapp.* import scala.build.input.Inputs import scala.build.internal.Constants import scala.build.{Logger, Os} import scala.cli.CurrentParams import scala.cli.commands.ScalaCommand import scala.cli.commands.setupide.SetupIde import scala.cli.commands.shared.HelpCommandGroup object Clean extends ScalaCommand[CleanOptions] { override def group: String = HelpCommandGroup.Main.toString override def scalaSpecificationLevel = SpecificationLevel.IMPLEMENTATION override def runCommand(options: CleanOptions, args: RemainingArgs, logger: Logger): Unit = { val inputs = Inputs( args.all, Os.pwd, defaultInputs = () => Inputs.default(), forcedWorkspace = options.workspace.forcedWorkspaceOpt, allowRestrictedFeatures = allowRestrictedFeatures, extraClasspathWasPassed = false ) match { case Left(message) => System.err.println(message) sys.exit(1) case Right(i) => i } CurrentParams.workspaceOpt = Some(inputs.workspace) val workDir = inputs.workspace / Constants.workspaceDirName val (_, bspEntry) = SetupIde.bspDetails(inputs.workspace, options.bspFile) if (os.exists(workDir)) { logger.debug(s"Working directory: $workDir") if (os.isDir(workDir)) { logger.log(s"Removing $workDir") os.remove.all(workDir) } else logger.log(s"$workDir is not a directory, ignoring it.") } if (os.exists(bspEntry)) { logger.log(s"Removing $bspEntry") os.remove(bspEntry) } else logger.log(s"No BSP entry found, so ignoring.") } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/clean/CleanOptions.scala ================================================ package scala.cli.commands.clean import caseapp.* import scala.cli.ScalaCli.fullRunnerName import scala.cli.commands.shared.* // format: off @HelpMessage(CleanOptions.helpMessage, "", CleanOptions.detailedHelpMessage) final case class CleanOptions( @Recurse global: GlobalOptions = GlobalOptions(), @Recurse bspFile: SharedBspFileOptions = SharedBspFileOptions(), @Recurse workspace: SharedWorkspaceOptions = SharedWorkspaceOptions() ) extends HasGlobalOptions // format: on object CleanOptions { implicit lazy val parser: Parser[CleanOptions] = Parser.derive implicit lazy val help: Help[CleanOptions] = Help.derive val cmdName = "clean" private val helpHeader = "Clean the workspace." val helpMessage: String = HelpMessages.shortHelpMessage(cmdName, helpHeader) val detailedHelpMessage: String = s"""$helpHeader | |Passed inputs will establish the $fullRunnerName project, for which the workspace will be cleaned. | |${HelpMessages.commandDocWebsiteReference(cmdName)}""".stripMargin } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/compile/Compile.scala ================================================ package scala.cli.commands.compile import caseapp.* import caseapp.core.help.HelpFormat import java.io.File import scala.build.options.Scope import scala.build.{Build, BuildThreads, Builds, Logger} import scala.cli.CurrentParams import scala.cli.commands.setupide.SetupIde import scala.cli.commands.shared.{HelpCommandGroup, HelpGroup, SharedOptions} import scala.cli.commands.update.Update import scala.cli.commands.util.BuildCommandHelpers import scala.cli.commands.util.BuildCommandHelpers.* import scala.cli.commands.{CommandUtils, ScalaCommand, SpecificationLevel, WatchUtil} import scala.cli.config.Keys import scala.cli.packaging.Library.fullClassPathMaybeAsJar import scala.cli.util.ArgHelpers.* import scala.cli.util.ConfigDbUtils object Compile extends ScalaCommand[CompileOptions] with BuildCommandHelpers { override def group: String = HelpCommandGroup.Main.toString override def sharedOptions(options: CompileOptions): Option[SharedOptions] = Some(options.shared) override def buildOptions(options: CompileOptions): Some[scala.build.options.BuildOptions] = Some(options.buildOptions().orExit(options.shared.logger)) override def scalaSpecificationLevel: SpecificationLevel = SpecificationLevel.MUST val primaryHelpGroups: Seq[HelpGroup] = Seq( HelpGroup.Compilation, HelpGroup.Scala, HelpGroup.Java, HelpGroup.Watch, HelpGroup.CompilationServer ) override def helpFormat: HelpFormat = super.helpFormat.withPrimaryGroups(primaryHelpGroups) override def runCommand(options: CompileOptions, args: RemainingArgs, logger: Logger): Unit = { val buildOptions = buildOptionsOrExit(options) val inputs = options.shared.inputs(args.all).orExit(logger) CurrentParams.workspaceOpt = Some(inputs.workspace) SetupIde.runSafe( options.shared, inputs, logger, buildOptions, Some(name), args.all ) if (CommandUtils.shouldCheckUpdate) Update.checkUpdateSafe(logger) val cross = options.cross.cross.getOrElse(false) if (options.printClassPath && cross) { System.err.println(s"Error: cannot specify both --print-class-path and --cross") sys.exit(1) } def postBuild(builds: Builds, allowExit: Boolean): Unit = { val failed = builds.all.exists { case _: Build.Failed => true case _ => false } val cancelled = builds.all.exists { case _: Build.Cancelled => true case _ => false } if (failed) { System.err.println("Compilation failed") if (allowExit) sys.exit(1) } else if (cancelled) { System.err.println("Compilation cancelled") if (allowExit) sys.exit(1) } else { val successulBuildOpt = for { build <- builds.get(Scope.Test).orElse(builds.get(Scope.Main)) s <- build.successfulOpt } yield s if (options.printClassPath) for (s <- successulBuildOpt) { val cp = s.fullClassPathMaybeAsJar(options.shared.asJar) .map(_.toString) .mkString(File.pathSeparator) println(cp) } successulBuildOpt.foreach(_.copyOutput(options.shared)) } } val threads = BuildThreads.create() val compilerMaker = options.shared.compilerMaker(threads) val configDb = ConfigDbUtils.configDb.orExit(logger) val actionableDiagnostics = options.shared.logging.verbosityOptions.actions.orElse( configDb.get(Keys.actions).getOrElse(None) ) val shouldBuildTestScope = options.shared.scope.test.getOrElse(false) if (options.watch.watchMode) { val watcher = Build.watch( inputs, buildOptions, compilerMaker, None, logger, crossBuilds = cross, buildTests = shouldBuildTestScope, partial = None, actionableDiagnostics = actionableDiagnostics, postAction = () => WatchUtil.printWatchMessage() ) { res => for (builds <- res.orReport(logger)) postBuild(builds, allowExit = false) } try WatchUtil.waitForCtrlC(() => watcher.schedule()) finally watcher.dispose() } else { val res = Build.build( inputs, buildOptions, compilerMaker, None, logger, crossBuilds = cross, buildTests = shouldBuildTestScope, partial = None, actionableDiagnostics = actionableDiagnostics ) val builds = res.orExit(logger) postBuild(builds, allowExit = true) } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/compile/CompileOptions.scala ================================================ package scala.cli.commands.compile import caseapp.* import caseapp.core.help.Help import scala.cli.commands.shared.* import scala.cli.commands.tags @HelpMessage(CompileOptions.helpMessage, "", CompileOptions.detailedHelpMessage) // format: off final case class CompileOptions( @Recurse shared: SharedOptions = SharedOptions(), @Recurse watch: SharedWatchOptions = SharedWatchOptions(), @Recurse cross: CrossOptions = CrossOptions(), @Group(HelpGroup.Compilation.toString) @Name("p") @Name("printClasspath") @HelpMessage("Print the resulting class path") @Tag(tags.should) @Tag(tags.inShortHelp) printClassPath: Boolean = false ) extends HasSharedOptions with HasSharedWatchOptions // format: on object CompileOptions { implicit lazy val parser: Parser[CompileOptions] = Parser.derive implicit lazy val help: Help[CompileOptions] = Help.derive val cmdName = "compile" private val helpHeader = "Compile Scala code." val helpMessage: String = HelpMessages.shortHelpMessage(cmdName, helpHeader) val detailedHelpMessage: String = s"""$helpHeader | |${HelpMessages.commandConfigurations(cmdName)} | |${HelpMessages.acceptedInputs} | |${HelpMessages.commandDocWebsiteReference(cmdName)}""".stripMargin } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/config/Config.scala ================================================ package scala.cli.commands.config import caseapp.core.RemainingArgs import caseapp.core.help.HelpFormat import scala.build.Ops.* import scala.build.errors.{BuildException, CompositeBuildException, MalformedCliInputError} import scala.build.internal.util.WarningMessages import scala.build.internals.FeatureType import scala.build.{Directories, Logger} import scala.cli.commands.publish.ConfigUtil.* import scala.cli.commands.shared.HelpGroup import scala.cli.commands.{ScalaCommand, SpecificationLevel} import scala.cli.config.* import scala.cli.util.ArgHelpers.* import scala.cli.util.ConfigDbUtils object Config extends ScalaCommand[ConfigOptions] { override def scalaSpecificationLevel: SpecificationLevel = SpecificationLevel.MUST override def helpFormat: HelpFormat = super.helpFormat .withHiddenGroup(HelpGroup.Java) .withPrimaryGroup(HelpGroup.Config) override def runCommand(options: ConfigOptions, args: RemainingArgs, logger: Logger): Unit = { val directories = Directories.directories if (options.dump) { val content = os.read.bytes(directories.dbPath) System.out.write(content) } else { val db = ConfigDbUtils.configDb.orExit(logger) def unrecognizedKey(key: String): Nothing = { System.err.println(s"Error: unrecognized key $key") sys.exit(1) } args.all match { case Seq() => if (options.createPgpKey) { if (options.pgpPassword.isEmpty) { logger.error( s"--pgp-password not specified, use 'none' to create an unprotected keychain or 'random' to generate a password" ) sys.exit(1) } val coursierCache = options.coursier.coursierCache(logger) val secKeyEntry = Keys.pgpSecretKey val pubKeyEntry = Keys.pgpPublicKey val mail = options.email .filter(_.trim.nonEmpty) .orElse { db.get(Keys.userEmail) .wrapConfigException .orExit(logger) } .getOrElse { logger.error( s"--email ... not specified, and ${Keys.userEmail.fullName} not set (either is required to generate a PGP key)" ) sys.exit(1) } val passwordOpt = if (options.pgpPassword.contains("none")) None else if (options.pgpPassword.contains("random")) Some(ThrowawayPgpSecret.pgpPassPhrase()) else options.pgpPassword.map(scala.cli.signing.shared.Secret.apply) val (pgpPublic, pgpSecret) = ThrowawayPgpSecret.pgpSecret( mail, passwordOpt, logger, coursierCache, options.jvm, options.coursier, options.scalaSigning.cliOptions() ).orExit(logger) db.set(secKeyEntry, PasswordOption.Value(pgpSecret.toConfig)) db.set(pubKeyEntry, PasswordOption.Value(pgpPublic.toConfig)) db.save(directories.dbPath.toNIO) .wrapConfigException .orExit(logger) logger.message("PGP keychains written to config") if (options.pgpPassword.contains("random")) passwordOpt.foreach { password => println( s"""Password: ${password.value} |Don't lose it! |""".stripMargin ) } } else { System.err.println("No argument passed") sys.exit(1) } case Seq(name, values @ _*) => Keys.map.get(name) match { case None => unrecognizedKey(name) case Some(powerEntry) if (powerEntry.isRestricted || powerEntry.isExperimental) && !allowRestrictedFeatures => logger.error(WarningMessages.powerConfigKeyUsedInSip(powerEntry)) sys.exit(1) case Some(entry) => if entry.isExperimental && !shouldSuppressExperimentalFeatureWarnings then logger.experimentalWarning(entry.fullName, FeatureType.ConfigKey) if (values.isEmpty) if (options.unset) { db.remove(entry) db.save(directories.dbPath.toNIO) .wrapConfigException .orExit(logger) } else { val valueOpt = db.getAsString(entry) .wrapConfigException .orExit(logger) valueOpt match { case Some(value) => for (v <- value) if (options.passwordValue && entry.isPasswordOption) PasswordOption.parse(v) match { case Left(err) => System.err.println(err) sys.exit(1) case Right(passwordOption) => val password = passwordOption.getBytes System.out.write(password.value) } else println(v) case None => logger.debug(s"No value found for $name") } } else { def parseSecret(input: String): Either[BuildException, Option[PasswordOption]] = if (input.trim.isEmpty) Right(None) else PasswordOption.parse(input) .left.map(err => new MalformedCliInputError(s"Malformed secret value '$input': $err") ) .map(Some(_)) entry match { case Keys.repositoryCredentials => if (options.unset) values match { case Seq(host) => val valueOpt = db.get(Keys.repositoryCredentials) .wrapConfigException .orExit(logger) def notFound(): Unit = logger.message( s"No ${Keys.repositoryCredentials.fullName} found for host $host" ) valueOpt match { case None => notFound() case Some(credList) => val idx = credList.indexWhere(_.host == host) if (idx < 0) notFound() else { val updatedCredList = credList.take(idx) ::: credList.drop(idx + 1) db.set(Keys.repositoryCredentials, updatedCredList) db.save(directories.dbPath.toNIO).wrapConfigException.orExit(logger) } } case _ => System.err.println( s"Usage: $progName config --remove ${Keys.repositoryCredentials.fullName} host" ) sys.exit(1) } else { val (host, rawUser, rawPassword, realmOpt) = values match { case Seq(host, rawUser, rawPassword) => (host, rawUser, rawPassword, None) case Seq(host, rawUser, rawPassword, realm) => (host, rawUser, rawPassword, Some(realm)) case _ => System.err.println( s"Usage: $progName config ${Keys.repositoryCredentials.fullName} host user password [realm]" ) System.err.println( "Note that user and password are assumed to be secrets, specified like value:... or env:ENV_VAR_NAME, see https://scala-cli.virtuslab.org/docs/reference/password-options for more details" ) sys.exit(1) } val (userOpt, passwordOpt) = (parseSecret(rawUser), parseSecret(rawPassword)) .traverseN .left.map(CompositeBuildException(_)) .orExit(logger) val credentials = if (options.passwordValue) RepositoryCredentials( host, userOpt.map(user => PasswordOption.Value(user.get())), passwordOpt.map(password => PasswordOption.Value(password.get())), realm = realmOpt, optional = options.optional, matchHost = options.matchHost.orElse(Some(true)), httpsOnly = options.httpsOnly, passOnRedirect = options.passOnRedirect ) else RepositoryCredentials( host, userOpt, passwordOpt, realm = realmOpt, optional = options.optional, matchHost = options.matchHost.orElse(Some(true)), httpsOnly = options.httpsOnly, passOnRedirect = options.passOnRedirect ) val previousValueOpt = db.get(Keys.repositoryCredentials).wrapConfigException.orExit(logger) val newValue = credentials :: previousValueOpt.getOrElse(Nil) db.set(Keys.repositoryCredentials, newValue) } case Keys.publishCredentials => val (host, rawUser, rawPassword, realmOpt) = values match { case Seq(host, rawUser, rawPassword) => (host, rawUser, rawPassword, None) case Seq(host, rawUser, rawPassword, realm) => (host, rawUser, rawPassword, Some(realm)) case _ => System.err.println( s"Usage: $progName config ${Keys.publishCredentials.fullName} host user password [realm]" ) System.err.println( "Note that user and password are assumed to be secrets, specified like value:... or env:ENV_VAR_NAME, see https://scala-cli.virtuslab.org/docs/reference/password-options for more details" ) sys.exit(1) } val (userOpt, passwordOpt) = (parseSecret(rawUser), parseSecret(rawPassword)) .traverseN .left.map(CompositeBuildException(_)) .orExit(logger) val credentials = if (options.passwordValue) PublishCredentials( host, userOpt.map(user => PasswordOption.Value(user.get())), passwordOpt.map(password => PasswordOption.Value(password.get())), realm = realmOpt ) else PublishCredentials(host, userOpt, passwordOpt, realm = realmOpt) val previousValueOpt = db.get(Keys.publishCredentials).wrapConfigException.orExit(logger) val newValue = credentials :: previousValueOpt.getOrElse(Nil) db.set(Keys.publishCredentials, newValue) case _ => val finalValues = if (options.passwordValue && entry.isPasswordOption) values.map { input => PasswordOption.parse(input) match { case Left(err) => System.err.println(err) sys.exit(1) case Right(passwordOption) => PasswordOption.Value(passwordOption.get()).asString.value } } else values checkIfAskForUpdate(entry, finalValues, db, options) db.setFromString(entry, finalValues) .wrapConfigException .orExit(logger) } db.save(directories.dbPath.toNIO) .wrapConfigException .orExit(logger) } } } } logger.flushExperimentalWarnings } /** Check whether to ask for an update depending on the provided key. */ private def checkIfAskForUpdate( entry: Key[?], newValues: Seq[String], db: ConfigDb, options: ConfigOptions ): Unit = entry match { case listEntry: Key.StringListEntry => val previousValue = db.get(listEntry).wrapConfigException.orExit(logger).getOrElse(Nil) confirmUpdateValue( listEntry.fullName, previousValue, newValues, options ).wrapConfigException.orExit(logger) case _ => () } /** If the new value is different from the previous value, ask user for confirmation or suggest to * use --force option. If the new value is the same as the previous value, confirm the operation. * If force option is provided, skip the confirmation. */ private def confirmUpdateValue( keyFullName: String, previousValues: Seq[String], newValues: Seq[String], options: ConfigOptions ): Either[Exception, Unit] = val (newValuesStr, previousValueStr) = (newValues.mkString(", "), previousValues.mkString(", ")) val shouldUpdate = !options.force && newValuesStr != previousValueStr && previousValues.nonEmpty if shouldUpdate then val interactive = options.global.logging.verbosityOptions.interactiveInstance() val msg = s"Do you want to change the key '$keyFullName' from '$previousValueStr' to '$newValuesStr'?" interactive.confirmOperation(msg) match { case Some(true) => Right(()) case _ => Left(new Exception( s"Unable to change the value for the key: '$keyFullName' from '$previousValueStr' to '$newValuesStr' without the force flag. Please pass -f or --force to override." )) } else Right(()) } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/config/ConfigOptions.scala ================================================ package scala.cli.commands.config import caseapp.* import scala.build.internals.ConsoleUtils.ScalaCliConsole import scala.cli.ScalaCli.{allowRestrictedFeatures, fullRunnerName, progName} import scala.cli.commands.pgp.PgpScalaSigningOptions import scala.cli.commands.shared.* import scala.cli.commands.tags import scala.cli.config.{Key, Keys} // format: off @HelpMessage(ConfigOptions.helpMessage, "", ConfigOptions.detailedHelpMessage) final case class ConfigOptions( @Recurse global: GlobalOptions = GlobalOptions(), @Recurse coursier: CoursierOptions = CoursierOptions(), @Recurse jvm: SharedJvmOptions = SharedJvmOptions(), @Recurse scalaSigning: PgpScalaSigningOptions = PgpScalaSigningOptions(), @Group(HelpGroup.Config.toString) @HelpMessage("Dump config DB as JSON") @Hidden @Tag(tags.implementation) @Tag(tags.inShortHelp) dump: Boolean = false, @Group(HelpGroup.Config.toString) @HelpMessage("Create PGP keychain in config") @Tag(tags.inShortHelp) @Tag(tags.experimental) createPgpKey: Boolean = false, @Group(HelpGroup.Config.toString) @HelpMessage("A password used to encode the private PGP keychain") @Tag(tags.experimental) @ValueDescription("YOUR_PASSWORD|random|none") @ExtraName("passphrase") pgpPassword: Option[String] = None, @Group(HelpGroup.Config.toString) @HelpMessage("Email used to create the PGP keychains in config") @Tag(tags.experimental) email: Option[String] = None, @Group(HelpGroup.Config.toString) @HelpMessage( """When accessing config's content print the password value rather than how to get the password |When saving an entry in config save the password value rather than how to get the password |e.g. print/save the value of environment variable ENV_VAR rather than "env:ENV_VAR" |""".stripMargin) @Tag(tags.restricted) @Tag(tags.inShortHelp) passwordValue: Boolean = false, @Group(HelpGroup.Config.toString) @HelpMessage("Remove an entry from config") @Tag(tags.inShortHelp) @Tag(tags.should) @ExtraName("remove") unset: Boolean = false, @Group(HelpGroup.Config.toString) @HelpMessage("For repository.credentials and publish.credentials, whether these credentials should be HTTPS only (default: true)") @Tag(tags.restricted) @Tag(tags.inShortHelp) httpsOnly: Option[Boolean] = None, @Group(HelpGroup.Config.toString) @HelpMessage("For repository.credentials, whether to use these credentials automatically based on the host") @Tag(tags.restricted) @Tag(tags.inShortHelp) matchHost: Option[Boolean] = None, @Group(HelpGroup.Config.toString) @HelpMessage("For repository.credentials, whether to use these credentials are optional") @Tag(tags.restricted) @Tag(tags.inShortHelp) optional: Option[Boolean] = None, @Group(HelpGroup.Config.toString) @HelpMessage("For repository.credentials, whether to use these credentials should be passed upon redirection") @Tag(tags.restricted) @Tag(tags.inShortHelp) passOnRedirect: Option[Boolean] = None, @Group(HelpGroup.Config.toString) @HelpMessage("Force overwriting values for key") @ExtraName("f") @Tag(tags.inShortHelp) @Tag(tags.should) force: Boolean = false ) extends HasGlobalOptions // format: on object ConfigOptions { implicit lazy val parser: Parser[ConfigOptions] = Parser.derive implicit lazy val help: Help[ConfigOptions] = Help.derive private val helpHeader: String = s"Configure global settings for $fullRunnerName." private val cmdName = "config" val helpMessage: String = s"""$helpHeader | |Available keys: | ${configKeyMessages(includeHidden = false).mkString(s"${System.lineSeparator} ")} | |${HelpMessages.commandFullHelpReference(cmdName)} |${HelpMessages.commandDocWebsiteReference(cmdName)}""".stripMargin private def configKeyMessages(includeHidden: Boolean): Seq[String] = { val allKeys: Seq[Key[?]] = Keys.map.values.toSeq val allowedKeys: Seq[Key[?]] = if allowRestrictedFeatures then allKeys else allKeys.filterNot(k => k.isRestricted || k.isExperimental) val keys: Seq[Key[?]] = if includeHidden then allowedKeys else allowedKeys.filterNot(k => k.hidden || k.isExperimental) val maxFullNameLength = keys.map(_.fullName.length).max keys.sortBy(_.fullName) .map { key => val currentKeyFullNameLength = maxFullNameLength - key.fullName.length val extraSpaces = if currentKeyFullNameLength > 0 then " " * currentKeyFullNameLength else "" val hiddenOrExperimentalString = if key.hidden then s"${ScalaCliConsole.GRAY}(hidden)${Console.RESET} " else if key.isRestricted then s"${ScalaCliConsole.GRAY}(power)${Console.RESET} " else if key.isExperimental then s"${ScalaCliConsole.GRAY}(experimental)${Console.RESET} " else "" s"${Console.YELLOW}${key.fullName}${Console.RESET}$extraSpaces $hiddenOrExperimentalString${key.description}" } } val detailedHelpMessage: String = s"""$helpHeader | |Syntax: | ${Console.BOLD}$progName $cmdName key value${Console.RESET} |For example, to globally set the interactive mode: | ${Console.BOLD}$progName $cmdName interactive true${Console.RESET} | |Available keys: | ${configKeyMessages(includeHidden = true).mkString(s"${System.lineSeparator} ")} | |${HelpMessages.commandDocWebsiteReference(cmdName)}""".stripMargin } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/config/ThrowawayPgpSecret.scala ================================================ package scala.cli.commands.config import coursier.cache.FileCache import coursier.util.Task import java.security.SecureRandom import scala.build.EitherCps.{either, value} import scala.build.errors.BuildException import scala.build.{Logger, options as bo} import scala.cli.commands.pgp.PgpProxyMaker import scala.cli.commands.shared.{CoursierOptions, SharedJvmOptions} import scala.cli.errors.PgpError import scala.cli.signing.shared.Secret import scala.util.Properties object ThrowawayPgpSecret { private val secretChars = (('a' to 'z') ++ ('A' to 'Z') ++ ('0' to '9') ++ Seq('$', '/', '*', '&', '\'', '"', '!', '(', ')', '-', '_', '\\', ';', '.', ':', '=', '+', '?', ',', '%')).toVector private def secretChars(rng: SecureRandom): Iterator[Char] = Iterator.continually { val idx = rng.nextInt(secretChars.length) secretChars(idx) } def pgpPassPhrase(): Secret[String] = { val random = new SecureRandom Secret(secretChars(random).take(32).mkString) } def pgpSecret( mail: String, password: Option[Secret[String]], logger: Logger, cache: FileCache[Task], jvmOptions: SharedJvmOptions, coursierOptions: CoursierOptions, signingCliOptions: bo.ScalaSigningCliOptions ): Either[BuildException, (Secret[String], Secret[String])] = either { val dir = os.temp.dir(perms = if (Properties.isWin) null else "rwx------") val pubKey = dir / "pub" val secKey = dir / "sec" val retCode = value { (new PgpProxyMaker).get( signingCliOptions.forceExternal.getOrElse(false) ).createKey( pubKey.toString, secKey.toString, mail, logger.verbosity <= 0, password.map(_.value), cache, logger, jvmOptions, coursierOptions, signingCliOptions ) } def cleanUp(): Unit = os.remove.all(dir) if (retCode == 0) try (Secret(os.read(pubKey)), Secret(os.read(secKey))) finally cleanUp() else { cleanUp() value { Left { new PgpError( s"Failed to create PGP key pair (see messages above, scala-cli-signing return code: $retCode)" ) } } } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/default/Default.scala ================================================ package scala.cli.commands.default import caseapp.core.RemainingArgs import caseapp.core.help.RuntimeCommandsHelp import scala.build.Logger import scala.build.input.{Inputs, ScalaCliInvokeData, SubCommand} import scala.cli.commands.ScalaCommandWithCustomHelp import scala.cli.commands.repl.{Repl, ReplOptions} import scala.cli.commands.run.{Run, RunOptions} import scala.cli.commands.shared.{HelpCommandGroup, SharedOptions} import scala.cli.commands.version.{Version, VersionOptions} class Default(actualHelp: => RuntimeCommandsHelp) extends ScalaCommandWithCustomHelp[DefaultOptions](actualHelp) { private lazy val defaultCommandHelp: String = s""" |When no subcommand is passed explicitly, an implicit subcommand is used based on context: | - if the '--version' option is passed, it prints the 'version' subcommand output, unmodified by any other options | - if any inputs were passed, it defaults to the 'run' subcommand | - additionally, when no inputs were passed, it defaults to the 'run' subcommand in the following scenarios: | - if a snippet was passed with any of the '--execute*' options | - if a main class was passed with the '--main-class' option alongside an extra '--classpath' | - otherwise, if no inputs were passed, it defaults to the 'repl' subcommand""".stripMargin override def customHelp(showHidden: Boolean): String = super.customHelp(showHidden) + defaultCommandHelp override def scalaSpecificationLevel = SpecificationLevel.MUST override def group: String = HelpCommandGroup.Main.toString override def sharedOptions(options: DefaultOptions): Option[SharedOptions] = Some(options.shared) private[cli] var rawArgs = Array.empty[String] override def invokeData: ScalaCliInvokeData = super.invokeData.copy(subCommand = SubCommand.Default) override def runCommand(options: DefaultOptions, args: RemainingArgs, logger: Logger): Unit = // can't fully re-parse and redirect to Version because of --cli-version and --scala-version clashing if options.version then Version.runCommand( options = VersionOptions( global = options.shared.global, offline = options.shared.coursier.getOffline(logger).getOrElse(false) ), args = args, logger = logger ) else { val shouldDefaultToRun = args.remaining.nonEmpty || options.shared.snippet.executeScript.nonEmpty || options.shared.snippet.executeScala.nonEmpty || options.shared.snippet.executeJava.nonEmpty || options.shared.snippet.executeMarkdown.nonEmpty || (options.shared.extraClasspathWasPassed && options.sharedRun.mainClass.mainClass.nonEmpty) if shouldDefaultToRun then RunOptions.parser else ReplOptions.parser }.parse(options.legacyScala.filterNonDeprecatedArgs(rawArgs, progName, logger).toSeq) match case Left(e) => error(e) case Right((replOptions: ReplOptions, _)) => Repl.runCommand(replOptions, args, logger) case Right((runOptions: RunOptions, _)) => Run.runCommand( runOptions, args.remaining, args.unparsed, () => Inputs.default(), logger, invokeData ) } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/default/DefaultFile.scala ================================================ package scala.cli.commands.default import caseapp.core.RemainingArgs import java.io.File import scala.build.Logger import scala.cli.commands.ScalaCommand import scala.cli.internal.Constants import scala.util.Using object DefaultFile extends ScalaCommand[DefaultFileOptions] { override def hidden = true override def scalaSpecificationLevel = SpecificationLevel.RESTRICTED private def readDefaultFile(path: String): Array[Byte] = { val resourcePath = Constants.defaultFilesResourcePath + "/" + path val cl = Thread.currentThread().getContextClassLoader val resUrl = cl.getResource(resourcePath) if (resUrl == null) sys.error(s"Should not happen - resource $resourcePath not found") Using.resource(resUrl.openStream())(_.readAllBytes()) } final case class DefaultFile( path: os.SubPath, content: () => Array[Byte] ) { def printablePath: String = path.segments.mkString(File.separator) } def defaultWorkflow: Array[Byte] = readDefaultFile("workflows/default.yml") def defaultGitignore: Array[Byte] = readDefaultFile("gitignore") val defaultFiles = Map( "workflow" -> DefaultFile(os.sub / ".github" / "workflows" / "ci.yml", () => defaultWorkflow), "gitignore" -> DefaultFile(os.sub / ".gitignore", () => defaultGitignore) ) val defaultFilesByRelPath: Map[String, DefaultFile] = defaultFiles.flatMap { case (_, d) => // d.path.toString and d.printablePath differ on Windows (one uses '/', the other '\') Seq( d.path.toString -> d, d.printablePath -> d ) } private def unrecognizedFile(name: String, logger: Logger): Nothing = { logger.error( s"Error: unrecognized default file $name (available: ${defaultFiles.keys.toVector.sorted.mkString(", ")})" ) sys.exit(1) } override def runCommand( options: DefaultFileOptions, args: RemainingArgs, logger: Logger ): Unit = { lazy val allArgs = { val l = args.all if (l.isEmpty) { logger.error("No default file asked") sys.exit(1) } l } if (options.list || options.listIds) for ((name, d) <- defaultFiles.toVector.sortBy(_._1)) { if (options.listIds) println(name) if (options.list) println(d.printablePath) } else if (options.write) for (arg <- allArgs) defaultFiles.get(arg).orElse(defaultFilesByRelPath.get(arg)) match { case Some(f) => val dest = os.pwd / f.path if (!options.force && os.exists(dest)) { logger.error( s"Error: ${f.path} already exists. Pass --force to force erasing it." ) sys.exit(1) } if (options.force) os.write.over(dest, f.content(), createFolders = true) else os.write(dest, f.content(), createFolders = true) logger.message(s"Wrote ${f.path}") case None => unrecognizedFile(arg, logger) } else { if (allArgs.length > 1) { logger.error(s"Error: expected only one argument, got ${allArgs.length}") sys.exit(1) } val arg = allArgs.head val f = defaultFiles.get(arg).orElse(defaultFilesByRelPath.get(arg)).getOrElse { unrecognizedFile(arg, logger) } System.out.write(f.content()) } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/default/DefaultFileOptions.scala ================================================ package scala.cli.commands.default import caseapp.* import scala.cli.ScalaCli.fullRunnerName import scala.cli.commands.shared.{GlobalOptions, HasGlobalOptions, HelpGroup, HelpMessages} import scala.cli.commands.tags // format: off @HelpMessage( s"""Generates default files for a $fullRunnerName project (i.e. .gitignore). | |${HelpMessages.commandDocWebsiteReference("misc/default-file")}""".stripMargin) final case class DefaultFileOptions( @Recurse global: GlobalOptions = GlobalOptions(), @Group(HelpGroup.Default.toString) @HelpMessage("Write result to files rather than to stdout") @Tag(tags.restricted) write: Boolean = false, @Group(HelpGroup.Default.toString) @HelpMessage("List available default files") @Tag(tags.restricted) list: Boolean = false, @Group(HelpGroup.Default.toString) @HelpMessage("List available default file ids") @Tag(tags.restricted) listIds: Boolean = false, @Group(HelpGroup.Default.toString) @HelpMessage("Force overwriting destination files") @ExtraName("f") @Tag(tags.restricted) force: Boolean = false ) extends HasGlobalOptions // format: on object DefaultFileOptions { implicit lazy val parser: Parser[DefaultFileOptions] = Parser.derive implicit lazy val help: Help[DefaultFileOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/default/DefaultOptions.scala ================================================ package scala.cli.commands.default import caseapp.* import scala.cli.commands.repl.SharedReplOptions import scala.cli.commands.run.SharedRunOptions import scala.cli.commands.shared.{HasSharedOptions, SharedOptions} // format: off case class DefaultOptions( @Recurse shared: SharedOptions = SharedOptions(), @Recurse sharedRun: SharedRunOptions = SharedRunOptions(), @Recurse sharedRepl: SharedReplOptions = SharedReplOptions(), @Recurse legacyScala: LegacyScalaOptions = LegacyScalaOptions(), @Name("-version") version: Boolean = false ) extends HasSharedOptions // format: on object DefaultOptions { implicit lazy val parser: Parser[DefaultOptions] = Parser.derive implicit lazy val help: Help[DefaultOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/default/LegacyScalaOptions.scala ================================================ package scala.cli.commands.default import caseapp.* import caseapp.core.Indexed import scala.build.Logger import scala.cli.ScalaCli.{fullRunnerName, progName} import scala.cli.commands.bloop.BloopExit import scala.cli.commands.default.LegacyScalaOptions.* import scala.cli.commands.package0.Package import scala.cli.commands.shared.HelpGroup import scala.cli.commands.shared.HelpMessages.PowerString import scala.cli.commands.tags /** Options covering backwards compatibility with the old scala runner. */ // format: off case class LegacyScalaOptions( @Group(HelpGroup.LegacyScalaRunner.toString) @HelpMessage(s"Ignored legacy option. Deprecated equivalent of running a subsequent `$PowerString${Package.name}` command.") @Tag(tags.must) @Hidden @Name("-save") save: Option[Indexed[Boolean]] = None, @Group(HelpGroup.LegacyScalaRunner.toString) @HelpMessage("Ignored legacy option. Deprecated override canceling the `-nosave` option.") @Tag(tags.must) @Hidden @Name("-nosave") nosave: Option[Indexed[Boolean]] = None, @Group(HelpGroup.LegacyScalaRunner.toString) @HelpMessage("Ignored legacy option. Deprecated override defining how the runner should treat the input. Use the appropriate sub-command instead.") @Tag(tags.must) @Hidden @ValueDescription("object|script|jar|repl|guess") @Name("-howtorun") howToRun: Option[Indexed[String]] = None, @Group(HelpGroup.LegacyScalaRunner.toString) @HelpMessage("Ignored legacy option. Deprecated option allowing to preload inputs for the repl or command execution.") @Tag(tags.must) @Hidden @ValueDescription("file") I: Option[Indexed[List[String]]] = None, @Group(HelpGroup.LegacyScalaRunner.toString) @HelpMessage("Ignored legacy option. Deprecated option allowing to prevent the use of the legacy fsc compilation daemon.") @Tag(tags.must) @Hidden @Name("-nc") @Name("-nocompdaemon") noCompilationDaemon: Option[Indexed[Boolean]] = None, @Group(HelpGroup.LegacyScalaRunner.toString) @HelpMessage("Ignored legacy option. Deprecated option allowing to force the `run` mode on an input.") @Tag(tags.must) @Hidden @ValueDescription("file") @Name("-run") run: Option[Indexed[String]] = None, ) { // format: on extension [T](indexedOption: Option[Indexed[T]]) { private def findArg(args: Array[String]): Option[String] = indexedOption.flatMap(io => args.lift(io.index)) } def filterNonDeprecatedArgs( args: Array[String], progName: String, logger: Logger ): Array[String] = { val saveOptionString = save.findArg(args) val noSaveOptionString = nosave.findArg(args) val howToRunString = howToRun.findArg(args) val iString = I.findArg(args) val noCompilationDaemonString = noCompilationDaemon.findArg(args) val runString = run.findArg(args) val deprecatedArgs = Seq( saveOptionString, noSaveOptionString, howToRunString, iString, noCompilationDaemonString, runString ) .flatten val filteredArgs = args.filterNot(deprecatedArgs.contains) val filteredArgsString = filteredArgs.mkString(" ") saveOptionString.foreach { s => logger.message( s"""Deprecated option '$s' is ignored. |The compiled project files will be saved in the '.scala-build' directory in the project root folder. |If you need to produce an actual jar file, run the '$PowerString${Package.name}' sub-command as follows: | ${Console.BOLD}$progName $PowerString${Package .name} --library $filteredArgsString${Console.RESET}""".stripMargin ) } noSaveOptionString.foreach { ns => logger.message( s"""Deprecated option '$ns' is ignored. |A jar file is not saved unless the '$PowerString${Package .name}' sub-command is called.""".stripMargin ) } for { htrString <- howToRunString htrValue <- howToRun.map(_.value) } { logger.message(s"Deprecated option '$htrString' is ignored.".stripMargin) val passedValueExplanation = htrValue match { case v @ ("object" | "script" | "jar") => s"""$fullRunnerName does not support explicitly forcing an input to be run as '$v'. |Just make sure your inputs have the correct format and extension.""".stripMargin case "guess" => s"""$fullRunnerName does not support `guess` mode. |Just make sure your inputs have the correct format and extension.""".stripMargin case "repl" => s"""In order to explicitly run the repl, use the 'repl' sub-command. | ${Console.BOLD}$progName repl $filteredArgsString${Console.RESET} |""".stripMargin case invalid @ _ => s"""'$invalid' is not an accepted value for the '$htrString' option. |$fullRunnerName uses an equivalent of the old 'guess' mode by default at all times.""".stripMargin } logger.message(passedValueExplanation) logger.message( s"""Instead of the deprecated '$htrString' option, $fullRunnerName now uses a sub-command system. |To learn more, try viewing the help. | ${Console.BOLD}$progName -help${Console.RESET}""".stripMargin ) } for { optionName <- iString optionValues <- I.map(_.value) exampleReplInputs = optionValues.mkString(" ") } { logger.message(s"Deprecated option '$optionName' is ignored.".stripMargin) logger.message( s"""To preload the extra files for the repl, try passing them as inputs for the repl sub-command. | ${Console.BOLD}$progName repl $exampleReplInputs${Console.RESET} |""".stripMargin ) } noCompilationDaemonString.foreach { nc => logger.message(s"Deprecated option '$nc' is ignored.") logger.message("The script runner can no longer be picked as before.") } for { rString <- runString rValue <- run.map(_.value) } { logger.message(s"Deprecated option '$rString' is ignored.") logger.message( s"""$fullRunnerName does not support explicitly forcing the old `run` mode. |Just pass $rValue as input and make sure it has the correct format and extension. |i.e. to run a JAR, pass it as an input, just make sure it has the `.jar` extension and a main class. |For details on how inputs can be run with $fullRunnerName, check the `run` sub-command. | ${Console.BOLD}$progName run --help${Console.RESET} |""".stripMargin ) } filteredArgs } } object LegacyScalaOptions { implicit lazy val parser: Parser[LegacyScalaOptions] = Parser.derive implicit lazy val help: Help[LegacyScalaOptions] = Help.derive def yScriptRunnerWarning(yScriptRunnerKey: String, yScriptRunnerValue: Option[String]): String = { val valueSpecificMsg = yScriptRunnerValue match { case Some(v @ "default") => s"scala.tools.nsc.DefaultScriptRunner (the $v script runner) is no longer available." case Some(v @ "resident") => s"scala.tools.nsc.fsc.ResidentScriptRunner (the $v script runner) is no longer available." case Some(v @ "shutdown") => val bloopExitCommandName = BloopExit.names.headOption.map(_.mkString(" ")).getOrElse(BloopExit.name) s"""scala.tools.nsc.fsc.DaemonKiller (the $v script runner) is no longer available. |Did you want to stop the $fullRunnerName build server (Bloop) instead? |If so, consider using the following command: | ${Console.BOLD}$progName $PowerString$bloopExitCommandName${Console.RESET}""".stripMargin case Some(className) => s"Using $className as the script runner is no longer supported and will not be attempted." case _ => "" } s"""Deprecated option '$yScriptRunnerKey' is ignored. |The script runner can no longer be picked as before. |$valueSpecificMsg""".stripMargin } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/dependencyupdate/DependencyUpdate.scala ================================================ package scala.cli.commands.dependencyupdate import caseapp.* import caseapp.core.help.HelpFormat import scala.build.actionable.ActionableDependencyHandler import scala.build.actionable.ActionableDiagnostic.ActionableDependencyUpdateDiagnostic import scala.build.internals.ConsoleUtils.ScalaCliConsole.warnPrefix import scala.build.options.Scope import scala.build.{CrossSources, Logger, Position, Sources} import scala.cli.commands.shared.{HelpCommandGroup, HelpGroup, SharedOptions} import scala.cli.commands.{ScalaCommand, SpecificationLevel} import scala.cli.util.ArgHelpers.* object DependencyUpdate extends ScalaCommand[DependencyUpdateOptions] { override def group: String = HelpCommandGroup.Main.toString override def scalaSpecificationLevel: SpecificationLevel = SpecificationLevel.RESTRICTED override def helpFormat: HelpFormat = super.helpFormat.withPrimaryGroup(HelpGroup.Dependency) override def sharedOptions(options: DependencyUpdateOptions): Option[SharedOptions] = Some(options.shared) override def runCommand( options: DependencyUpdateOptions, args: RemainingArgs, logger: Logger ): Unit = { if options.shared.scope.test.nonEmpty then logger.message( s"""$warnPrefix Including the test scope does not change the behaviour of this command. |$warnPrefix Test dependencies are updated regardless.""".stripMargin ) val verbosity = options.shared.logging.verbosity val buildOptions = buildOptionsOrExit(options) val inputs = options.shared.inputs(args.all).orExit(logger) val (crossSources, _) = CrossSources.forInputs( inputs, Sources.defaultPreprocessors( buildOptions.archiveCache, buildOptions.internal.javaClassNameVersionOpt, () => buildOptions.javaHome().value.javaCommand ), logger, buildOptions.suppressWarningOptions, buildOptions.internal.exclude, download = buildOptions.downloader ).orExit(logger) val sharedOptions = crossSources.sharedOptions(buildOptions) val scopedSources = crossSources.scopedSources(buildOptions).orExit(logger) def generateActionableUpdateDiagnostic(scope: Scope) : Seq[ActionableDependencyUpdateDiagnostic] = { val sources = scopedSources.sources(scope, sharedOptions, inputs.workspace, logger).orExit(logger) if (verbosity >= 3) pprint.err.log(sources) val options = buildOptions.orElse(sources.buildOptions) ActionableDependencyHandler.createActionableDiagnostics(options, Some(logger)).orExit(logger) } val actionableMainUpdateDiagnostics = generateActionableUpdateDiagnostic(Scope.Main) val actionableTestUpdateDiagnostics = generateActionableUpdateDiagnostic(Scope.Test) val actionableUpdateDiagnostics = (actionableMainUpdateDiagnostics ++ actionableTestUpdateDiagnostics).distinct if (options.all) updateDependencies(actionableUpdateDiagnostics, logger) else { println("Updates") actionableUpdateDiagnostics.foreach(update => println( s" * ${update.dependencyModuleName} ${update.currentVersion} -> ${update.newVersion}" ) ) println(s"""|To update all dependencies run: | $baseRunnerName dependency-update --all""".stripMargin) } } private def updateDependencies( actionableUpdateDiagnostics: Seq[ActionableDependencyUpdateDiagnostic], logger: Logger ): Unit = { val groupedByFileDiagnostics = actionableUpdateDiagnostics.flatMap { diagnostic => diagnostic.positions.collect { case file: Position.File => file.path -> (file, diagnostic) } }.groupMap(_._1)(_._2) groupedByFileDiagnostics.foreach { case (Right(file), diagnostics) => val sortedByLine = diagnostics.sortBy(_._1.startPos._1).reverse val appliedDiagnostics = updateDependencies(file, sortedByLine) os.write.over(file, appliedDiagnostics) diagnostics.foreach(diagnostic => logger.message( s"Updated dependency ${diagnostic._2.dependencyModuleName}: ${diagnostic._2 .currentVersion} -> ${diagnostic._2.newVersion}" ) ) case (Left(file), diagnostics) => diagnostics.foreach { diagnostic => logger.message( s"Warning: $fullRunnerName can't update ${diagnostic._2.suggestion} in $file" ) } } } private def updateDependencies( file: os.Path, diagnostics: Seq[(Position.File, ActionableDependencyUpdateDiagnostic)] ): String = { val fileContent = os.read(file) val startIndicies = Position.Raw.lineStartIndices(fileContent) diagnostics.foldLeft(fileContent) { case (fileContent, (file, diagnostic)) => val (line, column) = (file.startPos._1, file.startPos._2) val (lineEnd, columnEnd) = (file.endPos._1, file.endPos._2) val startIndex = startIndicies(line) + column val endIndex = startIndicies(lineEnd) + columnEnd val newDependency = diagnostic.suggestion s"${fileContent.slice(0, startIndex)}$newDependency${fileContent.drop(endIndex)}" } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/dependencyupdate/DependencyUpdateOptions.scala ================================================ package scala.cli.commands.dependencyupdate import caseapp.* import caseapp.core.help.Help import scala.cli.commands.shared.{HasSharedOptions, HelpGroup, SharedOptions} import scala.cli.commands.tags // format: off @HelpMessage("Update dependency directives in the project") final case class DependencyUpdateOptions( @Recurse shared: SharedOptions = SharedOptions(), @Group(HelpGroup.Dependency.toString) @HelpMessage("Update all dependencies if a newer version was released") @Tag(tags.restricted) @Tag(tags.inShortHelp) all: Boolean = false ) extends HasSharedOptions // format: on object DependencyUpdateOptions { implicit lazy val parser: Parser[DependencyUpdateOptions] = Parser.derive implicit lazy val help: Help[DependencyUpdateOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/directories/Directories.scala ================================================ package scala.cli.commands.directories import caseapp.* import scala.build.Logger import scala.cli.commands.ScalaCommand object Directories extends ScalaCommand[DirectoriesOptions] { override def hidden: Boolean = true override def scalaSpecificationLevel = SpecificationLevel.RESTRICTED override def runCommand( options: DirectoriesOptions, args: RemainingArgs, logger: Logger ): Unit = { if (args.all.nonEmpty) { logger.error("The directories command doesn't accept arguments.") sys.exit(1) } val directories = scala.build.Directories.directories println("Local repository: " + directories.localRepoDir) println("Completions: " + directories.completionsDir) println("Virtual projects: " + directories.virtualProjectsDir) println("BSP sockets: " + directories.bspSocketDir) println("Bloop daemon directory: " + directories.bloopDaemonDir) println("Secrets directory: " + directories.secretsDir) } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/directories/DirectoriesOptions.scala ================================================ package scala.cli.commands.directories import caseapp.* import scala.cli.ScalaCli.fullRunnerName import scala.cli.commands.shared.{GlobalOptions, HasGlobalOptions} // format: off @HelpMessage(s"Prints directories used by $fullRunnerName.") final case class DirectoriesOptions( @Recurse global: GlobalOptions = GlobalOptions(), ) extends HasGlobalOptions // format: on object DirectoriesOptions { implicit lazy val parser: Parser[DirectoriesOptions] = Parser.derive implicit lazy val help: Help[DirectoriesOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/doc/Doc.scala ================================================ package scala.cli.commands.doc import caseapp.* import caseapp.core.help.HelpFormat import coursier.Fetch import dependency.* import java.io.File import scala.build.* import scala.build.EitherCps.{either, value} import scala.build.Ops.* import scala.build.compiler.{ScalaCompilerMaker, SimpleScalaCompilerMaker} import scala.build.errors.{BuildException, CompositeBuildException} import scala.build.interactive.InteractiveFileOps import scala.build.internal.Runner import scala.build.options.{BuildOptions, Scope} import scala.cli.CurrentParams import scala.cli.commands.shared.{HelpCommandGroup, HelpGroup, SharedOptions} import scala.cli.commands.util.BuildCommandHelpers import scala.cli.commands.{CommandUtils, ScalaCommand, SpecificationLevel} import scala.cli.config.Keys import scala.cli.errors.ScaladocGenerationFailedError import scala.cli.util.ArgHelpers.* import scala.cli.util.ConfigDbUtils import scala.util.Properties object Doc extends ScalaCommand[DocOptions] with BuildCommandHelpers { override def group: String = HelpCommandGroup.Main.toString override def sharedOptions(options: DocOptions): Option[SharedOptions] = Some(options.shared) override def buildOptions(options: DocOptions): Option[BuildOptions] = sharedOptions(options) .map(shared => shared.buildOptions().orExit(shared.logger)) override def helpFormat: HelpFormat = super.helpFormat.withPrimaryGroup(HelpGroup.Doc) override def scalaSpecificationLevel: SpecificationLevel = SpecificationLevel.MUST override def runCommand(options: DocOptions, args: RemainingArgs, logger: Logger): Unit = { val initialBuildOptions = buildOptionsOrExit(options) val inputs = options.shared.inputs(args.remaining).orExit(logger) CurrentParams.workspaceOpt = Some(inputs.workspace) val threads = BuildThreads.create() val maker = options.shared.compilerMaker(threads) val compilerMaker = ScalaCompilerMaker.IgnoreScala2(maker) val docCompilerMakerOpt = Some(SimpleScalaCompilerMaker("java", Nil, scaladoc = true)) val configDb = ConfigDbUtils.configDb.orExit(logger) val actionableDiagnostics = options.shared.logging.verbosityOptions.actions.orElse( configDb.get(Keys.actions).getOrElse(None) ) val cross = options.compileCross.cross.getOrElse(false) val withTestScope = options.shared.scope.test.getOrElse(false) val buildResult = Build.build( inputs, initialBuildOptions, compilerMaker, docCompilerMakerOpt, logger, crossBuilds = cross, buildTests = withTestScope, partial = None, actionableDiagnostics = actionableDiagnostics ) val docBuilds = buildResult.orExit(logger).allDoc docBuilds match { case b if b.forall(_.success) => val successfulBuilds = b.collect { case s: Build.Successful => s } if cross && successfulBuilds.nonEmpty then doDocCrossBuilds( logger = logger, outputOpt = options.output.filter(_.nonEmpty), force = options.force, allBuilds = successfulBuilds, extraArgs = args.unparsed, withTestScope = withTestScope ).orExit(logger) else doDoc( logger, options.output.filter(_.nonEmpty), options.force, successfulBuilds, args.unparsed, withTestScope ).orExit(logger) case b if b.exists(bb => !bb.success && !bb.cancelled) => logger.error("Compilation failed") sys.exit(1) case _ => logger.error("Build cancelled") sys.exit(1) } } /** Determines the output subdirectory name for one cross build when using `--cross`. Used so that * each Scala version (and optionally platform) gets a distinct directory. */ def crossDocSubdirName( crossParams: CrossBuildParams, multipleCrossGroups: Boolean, needsPlatformInSuffix: Boolean ): String = if !multipleCrossGroups then "" else if needsPlatformInSuffix then s"${crossParams.scalaVersion}_${crossParams.platform}" else crossParams.scalaVersion private def doDocCrossBuilds( logger: Logger, outputOpt: Option[String], force: Boolean, allBuilds: Seq[Build.Successful], extraArgs: Seq[String], withTestScope: Boolean ): Either[BuildException, Unit] = either { val crossBuildGroups = allBuilds.groupedByCrossParams.toSeq val multipleCrossGroups = crossBuildGroups.size > 1 if multipleCrossGroups then logger.message(s"Generating documentation for ${crossBuildGroups.size} cross builds...") val defaultName = "scala-doc" val baseOutputPath = outputOpt.map(p => os.Path(p, Os.pwd)).getOrElse(os.pwd / defaultName) val platforms = crossBuildGroups.map(_._1.platform).distinct val needsPlatformInSuffix = platforms.size > 1 value { crossBuildGroups .map { (crossParams, builds) => if multipleCrossGroups then logger.message(s"Generating documentation for ${crossParams.asString}...") val crossSubDir = Doc.crossDocSubdirName(crossParams, multipleCrossGroups, needsPlatformInSuffix) val groupOutputOpt = if crossSubDir.nonEmpty then Some((baseOutputPath / crossSubDir).toString) else outputOpt.filter(_.nonEmpty).orElse(Some(defaultName)) doDoc( logger = logger, outputOpt = groupOutputOpt, force = force, builds = builds, extraArgs = extraArgs, withTestScope = withTestScope ) } .sequence .left .map(CompositeBuildException(_)) .map(_ => ()) } } private def doDoc( logger: Logger, outputOpt: Option[String], force: Boolean, builds: Seq[Build.Successful], extraArgs: Seq[String], withTestScope: Boolean ): Either[BuildException, Unit] = either { def defaultName = "scala-doc" val dest = outputOpt.getOrElse(defaultName) val destPath = os.Path(dest, Os.pwd) val printableDest = CommandUtils.printablePath(destPath) def alreadyExistsCheck(): Either[BuildException, Unit] = { val alreadyExists = !force && os.exists(destPath) if (alreadyExists) builds.head.options.interactive.map { interactive => InteractiveFileOps.erasingPath(interactive, printableDest, destPath) { () => val msg = s"$printableDest already exists" logger.error(s"$msg. Pass -f or --force to force erasing it.") sys.exit(1) } } else Right(()) } value(alreadyExistsCheck()) val docJarPath = value(generateScaladocDirPath(builds, logger, extraArgs, withTestScope)) value(alreadyExistsCheck()) os.makeDir.all(destPath / os.up) if force then os.copy.over(docJarPath, destPath) else os.copy(docJarPath, destPath) val printableOutput = CommandUtils.printablePath(destPath) logger.message(s"Wrote Scaladoc to $printableOutput") } private def javadocBaseUrl(javaVersion: Int): String = if javaVersion >= 11 then s"https://docs.oracle.com/en/java/javase/$javaVersion/docs/api/java.base/" else s"https://docs.oracle.com/javase/$javaVersion/docs/api/" private def scaladocBaseUrl(scalaVersion: String): String = s"https://scala-lang.org/api/$scalaVersion/" // from https://github.com/VirtusLab/scala-cli/pull/103/files#diff-1039b442cbd23f605a61fdb9c3620b600aa4af6cab757932a719c54235d8e402R60 private[commands] def defaultScaladocArgs(scalaVersion: String, javaVersion: Int): Seq[String] = Seq( "-snippet-compiler:compile", "-Ygenerate-inkuire", "-external-mappings:" + s".*/scala/.*::scaladoc3::${scaladocBaseUrl(scalaVersion)}," + s".*/java/.*::javadoc::${javadocBaseUrl(javaVersion)}", "-author", "-groups" ) def generateScaladocDirPath( builds: Seq[Build.Successful], logger: Logger, extraArgs: Seq[String], withTestScope: Boolean ): Either[BuildException, os.Path] = either { val docContentDir = builds.head.scalaParams .map(sp => sp -> sp.scalaVersion.startsWith("2.")) match { case Some((_, true)) if withTestScope => builds.find(_.scope == Scope.Test).getOrElse(builds.head).project.scaladocDir case Some((_, true)) => builds.head.project.scaladocDir case Some((scalaParams, _)) => val res: Fetch.Result = value { Artifacts.fetchAnyDependencies( Seq(Positioned.none(dep"org.scala-lang::scaladoc:${scalaParams.scalaVersion}")), value(builds.head.options.finalRepositories), Some(scalaParams), logger, builds.head.options.finalCache, None ) } val destDir = builds.head.project.scaladocDir os.makeDir.all(destDir) val ext = if Properties.isWin then ".exe" else "" val baseArgs = Seq( "-classpath", builds .flatMap(_.fullCompileClassPath) .distinct .map(_.toString) .mkString(File.pathSeparator), "-d", destDir.toString ) val javaVersion = builds.head.options.javaHome().value.version val defaultArgs = if builds.head.options.notForBloopOptions.packageOptions.useDefaultScaladocOptions .getOrElse(true) then defaultScaladocArgs(scalaParams.scalaVersion, javaVersion) else Nil val args = baseArgs ++ builds.head.project.scalaCompiler.map(_.scalacOptions).getOrElse(Nil) ++ extraArgs ++ defaultArgs ++ builds.map(_.output.toString) val retCode = Runner.runJvm( (builds.head.options.javaHomeLocation().value / "bin" / s"java$ext").toString, Nil, // FIXME Allow to customize that? res.files.map(os.Path(_, os.pwd)), "dotty.tools.scaladoc.Main", args, logger, cwd = Some(builds.head.inputs.workspace) ).waitFor() if retCode == 0 then destDir else value(Left(new ScaladocGenerationFailedError(retCode))) case None => val destDir = builds.head.project.scaladocDir os.makeDir.all(destDir) val ext = if (Properties.isWin) ".exe" else "" val javaSources = builds .flatMap(b => b.sources.paths.map(_._1) ++ b.generatedSources.map(_.generated)) .distinct .filter(_.last.endsWith(".java")) val command = Seq( (builds.head.options.javaHomeLocation().value / "bin" / s"javadoc$ext").toString, "-d", destDir.toString, "-classpath", builds.flatMap(_.fullClassPath).distinct.map(_.toString).mkString(File.pathSeparator) ) ++ javaSources.map(_.toString) val retCode = Runner.run( command, logger, cwd = Some(builds.head.inputs.workspace) ).waitFor() if retCode == 0 then destDir else value(Left(new ScaladocGenerationFailedError(retCode))) } docContentDir } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/doc/DocOptions.scala ================================================ package scala.cli.commands.doc import caseapp.* import caseapp.core.help.Help import scala.cli.ScalaCli.fullRunnerName import scala.cli.commands.shared.{ CrossOptions, HasSharedOptions, HelpGroup, HelpMessages, SharedOptions } import scala.cli.commands.tags // format: off @HelpMessage(DocOptions.helpMessage, DocOptions.messageMd, DocOptions.detailedHelpMessage) final case class DocOptions( @Recurse shared: SharedOptions = SharedOptions(), @Recurse compileCross: CrossOptions = CrossOptions(), @Group(HelpGroup.Doc.toString) @Tag(tags.must) @HelpMessage("Set the destination path") @Name("o") output: Option[String] = None, @Group(HelpGroup.Doc.toString) @HelpMessage("Overwrite the destination directory, if it exists") @Tag(tags.must) @Name("f") force: Boolean = false, @Group(HelpGroup.Doc.toString) @HelpMessage(s"Control if $fullRunnerName should use default options for scaladoc, true by default. Use `--default-scaladoc-opts:false` to not include default options.") @Tag(tags.should) @ExtraName("defaultScaladocOpts") defaultScaladocOptions: Option[Boolean] = None ) extends HasSharedOptions // format: on object DocOptions { implicit lazy val parser: Parser[DocOptions] = Parser.derive implicit lazy val help: Help[DocOptions] = Help.derive val cmdName = "doc" private val helpHeader = "Generate Scaladoc documentation." val helpMessage: String = HelpMessages.shortHelpMessage(cmdName, helpHeader) val detailedHelpMessage: String = s"""Generate Scaladoc documentation. | |${HelpMessages.acceptedInputs} | |${HelpMessages.commandDocWebsiteReference(cmdName)}""".stripMargin val messageMd = s"By default, $fullRunnerName sets common `scaladoc` options and this mechanism can be disabled by using `--default-scaladoc-opts:false`." } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/export0/Export.scala ================================================ package scala.cli.commands.export0 import caseapp.* import caseapp.core.help.HelpFormat import coursier.cache.FileCache import coursier.util.{Artifact, Task} import scala.build.* import scala.build.EitherCps.{either, value} import scala.build.errors.BuildException import scala.build.input.Inputs import scala.build.internal.Constants import scala.build.options.{BuildOptions, Scope} import scala.cli.CurrentParams import scala.cli.commands.shared.{HelpGroup, SharedOptions} import scala.cli.commands.{ScalaCommand, SpecificationLevel} import scala.cli.exportCmd.* import scala.cli.util.ArgHelpers.* object Export extends ScalaCommand[ExportOptions] { override def scalaSpecificationLevel: SpecificationLevel = SpecificationLevel.EXPERIMENTAL override def helpFormat: HelpFormat = super.helpFormat.withPrimaryGroup(HelpGroup.BuildToolExport) private def prepareBuild( inputs: Inputs, buildOptions: BuildOptions, logger: Logger, verbosity: Int, scope: Scope ): Either[BuildException, (Sources, BuildOptions)] = either { logger.log("Preparing build") val (crossSources: CrossSources, _) = value { CrossSources.forInputs( inputs, Sources.defaultPreprocessors( buildOptions.archiveCache, buildOptions.internal.javaClassNameVersionOpt, () => buildOptions.javaHome().value.javaCommand ), logger, buildOptions.suppressWarningOptions, buildOptions.internal.exclude, download = buildOptions.downloader ) } val scopedSources: ScopedSources = value(crossSources.scopedSources(buildOptions)) val sources: Sources = scopedSources.sources( scope, crossSources.sharedOptions(buildOptions), inputs.workspace, logger ) .orExit(logger) if (verbosity >= 3) pprint.err.log(sources) val options0 = buildOptions.orElse(sources.buildOptions) (sources, options0) } // FIXME Auto-update those def sbtProjectDescriptor( extraSettings: Seq[String], sbtVersion: String, logger: Logger ): SbtProjectDescriptor = SbtProjectDescriptor(sbtVersion, extraSettings, logger) def mavenProjectDescriptor( mavenPluginVersion: String, mavenScalaPluginVersion: String, mavenExecPluginVersion: String, extraSettings: Seq[String], mavenGroupId: String, mavenArtifactId: String, mavenVersion: String, logger: Logger ): MavenProjectDescriptor = MavenProjectDescriptor( mavenPluginVersion, mavenScalaPluginVersion, mavenExecPluginVersion, extraSettings, mavenGroupId, mavenArtifactId, mavenVersion, logger ) def millProjectDescriptor( cache: FileCache[Task], projectName: Option[String], millVersion: String, logger: Logger ): MillProjectDescriptor = { val launcherArtifacts = Seq( os.rel / "mill" -> s"https://github.com/com-lihaoyi/mill/raw/$millVersion/mill", os.rel / "mill.bat" -> s"https://github.com/com-lihaoyi/mill/raw/$millVersion/mill.bat" ) val launcherTasks = launcherArtifacts.map { case (path, url) => val art = Artifact(url).withChanging(true) cache.file(art).run.flatMap { case Left(e) => Task.fail(e) case Right(f) => Task.delay { val content = os.read.bytes(os.Path(f, Os.pwd)) path -> content } } } val launchersTask = cache.logger.using(Task.gather.gather(launcherTasks)) val launchers = launchersTask.unsafeRun()(using cache.ec) MillProjectDescriptor( millVersion = millVersion, projectName = projectName, launchers = launchers, logger = logger ) } def jsonProjectDescriptor( projectName: Option[String], workspace: os.Path, logger: Logger ): JsonProjectDescriptor = JsonProjectDescriptor(projectName, workspace, logger) override def sharedOptions(opts: ExportOptions): Option[SharedOptions] = Some(opts.shared) override def runCommand(options: ExportOptions, args: RemainingArgs, logger: Logger): Unit = { if options.shared.scope.test.nonEmpty then { logger.error( s"""Including the test scope sources together with the main scope is currently not supported. |Note that test scope sources will still be exported as per the output build tool tests definition demands.""".stripMargin ) sys.exit(1) } val initialBuildOptions = buildOptionsOrExit(options) val output = options.output.getOrElse("dest") val dest = os.Path(output, os.pwd) val shouldExportToJson = options.json.getOrElse(false) val shouldExportJsonToStdout = shouldExportToJson && options.output.isEmpty if (!shouldExportJsonToStdout && os.exists(dest)) { logger.error( s"""Error: $dest already exists. |To change the destination output directory pass --output path or remove the destination directory first.""".stripMargin ) sys.exit(1) } val shouldExportToMill = options.mill.getOrElse(false) val shouldExportToSbt = options.sbt.getOrElse(false) val shouldExportToMaven = options.maven.getOrElse(false) val exportOptions = (if shouldExportToMill then List("Mill") else Nil) ++ (if shouldExportToSbt then List("SBT") else Nil) ++ (if shouldExportToMaven then List("Maven") else Nil) val exportOptionsString = exportOptions.mkString(", ") if exportOptions.length > 1 then { logger.error( s"""Error: Cannot export to more than one tool at once (currently chosen: $exportOptionsString). |Pick one build tool to export to.""".stripMargin ) sys.exit(1) } if (!shouldExportToJson) { val buildToolName = if (shouldExportToMill) "mill" else if (shouldExportToMaven) "maven" else "sbt" logger.message(s"Exporting to a $buildToolName project...") } else if (!shouldExportJsonToStdout) logger.message(s"Exporting to JSON...") val inputs = options.shared.inputs(args.all).orExit(logger) CurrentParams.workspaceOpt = Some(inputs.workspace) val baseOptions = initialBuildOptions .copy( scalaNativeOptions = initialBuildOptions.scalaNativeOptions.copy( maxDefaultNativeVersions = initialBuildOptions.scalaNativeOptions.maxDefaultNativeVersions ++ (if shouldExportToMill && Constants.scalaNativeVersion != Constants.maxScalaNativeForMillExport then val warningMsg = s"Mill export does not support Scala Native ${Constants.scalaNativeVersion}, ${Constants.maxScalaNativeForMillExport} should be used instead." List(Constants.maxScalaNativeForMillExport -> warningMsg) else Nil) ), mainClass = options.mainClass.mainClass.filter(_.nonEmpty) ) val (sourcesMain, optionsMain0) = prepareBuild( inputs, baseOptions, logger, options.shared.logging.verbosity, Scope.Main ) .orExit(logger) val (sourcesTest, optionsTest0) = prepareBuild( inputs, baseOptions, logger, options.shared.logging.verbosity, Scope.Test ) .orExit(logger) for { svMain <- optionsMain0.scalaOptions.scalaVersion svTest <- optionsTest0.scalaOptions.scalaVersion } if (svMain != svTest) { logger.error( s"""Detected different Scala versions in main and test scopes. |Please set the Scala version explicitly in the main and test scope with using directives or pass -S, --scala-version as parameter""".stripMargin ) sys.exit(1) } if ( optionsMain0.scalaOptions.scalaVersion.isEmpty && optionsTest0.scalaOptions.scalaVersion.nonEmpty ) { logger.error( s"""Detected that the Scala version is only set in test scope. |Please set the Scala version explicitly in the main and test scopes with using directives or pass -S, --scala-version as parameter""".stripMargin ) sys.exit(1) } if (shouldExportJsonToStdout) { val project = jsonProjectDescriptor(options.project, inputs.workspace, logger) .`export`(optionsMain0, optionsTest0, sourcesMain, sourcesTest) .orExit(logger) project.print(System.out) } else { val sbtVersion = options.sbtVersion.getOrElse(Constants.sbtVersion) val defaultMavenCompilerVersion = options.mvnVersion.getOrElse(Constants.mavenVersion) val defaultScalaMavenCompilerVersion = options.mvnScalaVersion.getOrElse(Constants.mavenScalaCompilerPluginVersion) val defaultMavenExecPluginVersion = options.mvnExecPluginVersion.getOrElse(Constants.mavenExecPluginVersion) val defaultMavenArtifactId = options.mvnAppArtifactId.getOrElse(Constants.mavenAppArtifactId) val defaultMavenGroupId = options.mvnAppGroupId.getOrElse(Constants.mavenAppGroupId) val defaultMavenVersion = options.mvnAppVersion.getOrElse(Constants.mavenAppVersion) def sbtProjectDescriptor0 = sbtProjectDescriptor(options.sbtSetting.map(_.trim).filter(_.nonEmpty), sbtVersion, logger) val projectDescriptor = if shouldExportToMill then millProjectDescriptor( cache = options.shared.coursierCache, projectName = options.project, millVersion = options.millVersion.getOrElse(Constants.millVersion), logger = logger ) else if shouldExportToMaven then mavenProjectDescriptor( mavenPluginVersion = defaultMavenCompilerVersion, mavenScalaPluginVersion = defaultScalaMavenCompilerVersion, mavenExecPluginVersion = defaultMavenExecPluginVersion, extraSettings = Nil, mavenGroupId = defaultMavenGroupId, mavenArtifactId = defaultMavenArtifactId, mavenVersion = defaultMavenVersion, logger = logger ) else if shouldExportToJson then jsonProjectDescriptor( projectName = options.project, workspace = inputs.workspace, logger = logger ) else // shouldExportToSbt isn't checked, as it's treated as default sbtProjectDescriptor0 val project = projectDescriptor.`export`(optionsMain0, optionsTest0, sourcesMain, sourcesTest) .orExit(logger) os.makeDir.all(dest) project.writeTo(dest) logger.message(s"Exported to: $dest") } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/export0/ExportOptions.scala ================================================ package scala.cli.commands.export0 import caseapp.* import scala.cli.ScalaCli.fullRunnerName import scala.cli.commands.shared.{ HasSharedOptions, HelpGroup, HelpMessages, MainClassOptions, SharedOptions } import scala.cli.commands.{Constants, tags} @HelpMessage(ExportOptions.helpMessage, "", ExportOptions.detailedHelpMessage) final case class ExportOptions( // FIXME There might be too many options for 'scala-cli export' there @Recurse shared: SharedOptions = SharedOptions(), @Recurse mainClass: MainClassOptions = MainClassOptions(), @Group(HelpGroup.BuildToolExport.toString) @Tag(tags.restricted) @Tag(tags.inShortHelp) @HelpMessage("Sets the export format to SBT") sbt: Option[Boolean] = None, @Group(HelpGroup.BuildToolExport.toString) @Tag(tags.experimental) @Tag(tags.inShortHelp) @HelpMessage("Sets the export format to Maven") @Name("mvn") maven: Option[Boolean] = None, @Group(HelpGroup.BuildToolExport.toString) @Tag(tags.restricted) @Tag(tags.inShortHelp) @HelpMessage("Sets the export format to Mill") mill: Option[Boolean] = None, @Tag(tags.restricted) @Tag(tags.inShortHelp) @Group(HelpGroup.BuildToolExport.toString) @HelpMessage("Sets the export format to Json") json: Option[Boolean] = None, @Name("setting") @Group(HelpGroup.BuildToolExport.toString) @Tag(tags.restricted) sbtSetting: List[String] = Nil, @Name("p") @Group(HelpGroup.BuildToolExport.toString) @Tag(tags.restricted) @HelpMessage("Project name to be used on Mill build file") project: Option[String] = None, @Group(HelpGroup.BuildToolExport.toString) @Tag(tags.restricted) @HelpMessage( s"Version of SBT to be used for the export (${Constants.defaultSbtVersion} by default)" ) sbtVersion: Option[String] = None, @Group(HelpGroup.BuildToolExport.toString) @Tag(tags.restricted) @HelpMessage( s"Version of Mill to be used for the export (${Constants.defaultMillVersion} by default)" ) millVersion: Option[String] = None, @Group(HelpGroup.BuildToolExport.toString) @Tag(tags.experimental) @HelpMessage( s"Version of Maven Compiler Plugin to be used for the export (${Constants.defaultMavenVersion} by default)" ) mvnVersion: Option[String] = None, @Group(HelpGroup.BuildToolExport.toString) @Tag(tags.experimental) @HelpMessage( s"Version of Maven Scala Plugin to be used for the export (${Constants.defaultMavenScalaCompilerPluginVersion} by default)" ) mvnScalaVersion: Option[String] = None, @Group(HelpGroup.BuildToolExport.toString) @Tag(tags.experimental) @HelpMessage( s"Version of Maven Exec Plugin to be used for the export (${Constants.defaultMavenExecPluginVersion} by default)" ) mvnExecPluginVersion: Option[String] = None, @Group(HelpGroup.BuildToolExport.toString) @Tag(tags.experimental) @HelpMessage("ArtifactId to be used for the maven export") mvnAppArtifactId: Option[String] = None, @Group(HelpGroup.BuildToolExport.toString) @Tag(tags.experimental) @HelpMessage("GroupId to be used for the maven export") mvnAppGroupId: Option[String] = None, @Group(HelpGroup.BuildToolExport.toString) @Tag(tags.experimental) @HelpMessage("Version to be used for the maven export") mvnAppVersion: Option[String] = None, @Name("o") @Group(HelpGroup.BuildToolExport.toString) @Tag(tags.restricted) output: Option[String] = None ) extends HasSharedOptions object ExportOptions { implicit lazy val parser: Parser[ExportOptions] = Parser.derive implicit lazy val help: Help[ExportOptions] = Help.derive private val helpHeader = "Export current project to an external build tool (like SBT or Mill) or to JSON." val helpMessage: String = s"""$helpHeader | |${HelpMessages.docsWebsiteReference}""".stripMargin val detailedHelpMessage: String = s"""$helpHeader | |The whole $fullRunnerName project should get exported along with its dependencies configuration. | |Unless otherwise configured, the default export format is SBT. | |${HelpMessages.acceptedInputs} | |${HelpMessages.docsWebsiteReference}""".stripMargin } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/fix/BuiltInRules.scala ================================================ package scala.cli.commands.fix import os.{BasePathImpl, FilePath} import scala.build.Ops.EitherMap2 import scala.build.errors.{BuildException, CompositeBuildException} import scala.build.input.* import scala.build.internal.Constants import scala.build.options.{BuildOptions, Scope, SuppressWarningOptions} import scala.build.preprocessing.directives.* import scala.build.preprocessing.{ExtractedDirectives, SheBang} import scala.build.{CrossSources, Logger, Position, Sources} import scala.cli.commands.util.CommandHelpers import scala.util.chaining.scalaUtilChainingOps object BuiltInRules extends CommandHelpers { private lazy val targetDirectivesKeysSet = DirectivesPreprocessingUtils.requireDirectiveHandlers .flatMap(_.keys.flatMap(_.nameAliases)).toSet private lazy val usingDirectivesKeysGrouped = DirectivesPreprocessingUtils.usingDirectiveHandlers .flatMap(_.keys) private lazy val usingDirectivesWithTestPrefixKeysGrouped = DirectivesPreprocessingUtils.usingDirectiveWithReqsHandlers .flatMap(_.keys) private lazy val directiveTestPrefix = "test." extension (strictDirective: StrictDirective) { private def hasTestPrefix: Boolean = strictDirective.key.startsWith(directiveTestPrefix) private def existsTestEquivalent: Boolean = !strictDirective.hasTestPrefix && usingDirectivesWithTestPrefixKeysGrouped .exists(_.nameAliases.contains(directiveTestPrefix + strictDirective.key)) } private val newLine: String = scala.build.internal.AmmUtil.lineSeparator def runRules( inputs: Inputs, buildOptions: BuildOptions, logger: Logger )(using ScalaCliInvokeData): Unit = { val (mainSources, testSources) = getProjectSources(inputs, logger) .left.map(CompositeBuildException(_)) .orExit(logger) val sourcesCount = mainSources.paths.length + mainSources.inMemory.length + testSources.paths.length + testSources.inMemory.length sourcesCount match case 0 => logger.message("No sources to migrate directives from.") logger.message("Nothing to do.") case 1 => logger.message("No need to migrate directives for a single source file project.") logger.message("Nothing to do.") case _ => migrateDirectives(inputs, buildOptions, mainSources, testSources, logger) } private def migrateDirectives( inputs: Inputs, buildOptions: BuildOptions, mainSources: Sources, testSources: Sources, logger: Logger ): Unit = { // Only initial inputs are used, new inputs discovered during processing of // CrossSources.forInput may be shared between projects val writableInputs: Seq[OnDisk] = inputs.flattened() .collect { case onDisk: OnDisk => onDisk } def isExtractedFromWritableInput(position: Option[Position.File]): Boolean = { val originOrPathOpt = position.map(_.path) originOrPathOpt match { case Some(Right(path)) => writableInputs.exists(_.path == path) case _ => false } } val projectFileContents = new StringBuilder() given LoggingUtilities(logger, inputs.workspace) // Deal with directives from the Main scope val (directivesFromWritableMainInputs, testDirectivesFromMain) = { val originalMainDirectives = getExtractedDirectives(mainSources, buildOptions.suppressWarningOptions) .filterNot(hasTargetDirectives) val transformedMainDirectives = unifyCorrespondingNameAliases(originalMainDirectives) val allDirectives = for { transformedMainDirective <- transformedMainDirectives directive <- transformedMainDirective.directives } yield directive val (testScopeDirectives, allMainDirectives) = allDirectives.partition(_.key.startsWith("test")) createFormattedLinesAndAppend(allMainDirectives, projectFileContents, isTest = false) ( transformedMainDirectives.filter(d => isExtractedFromWritableInput(d.position)), testScopeDirectives ) } // Deal with directives from the Test scope val directivesFromWritableTestInputs: Seq[TransformedTestDirectives] = if ( testSources.paths.nonEmpty || testSources.inMemory.nonEmpty || testDirectivesFromMain.nonEmpty ) { val originalTestDirectives = getExtractedDirectives(testSources, buildOptions.suppressWarningOptions) .filterNot(hasTargetDirectives) val transformedTestDirectives = unifyCorrespondingNameAliases(originalTestDirectives) .pipe(maybeTransformIntoTestEquivalent) val allDirectives = for { directivesWithTestPrefix <- transformedTestDirectives.map(_.withTestPrefix) directivesWithNoTestPrefixEquivalents <- transformedTestDirectives.map { _.noTestPrefixAvailable .filter(_.existsTestEquivalent) } directive <- directivesWithTestPrefix ++ directivesWithNoTestPrefixEquivalents ++ testDirectivesFromMain } yield directive createFormattedLinesAndAppend(allDirectives, projectFileContents, isTest = true) transformedTestDirectives .filter(ttd => isExtractedFromWritableInput(ttd.positions)) } else Seq(TransformedTestDirectives(Nil, Nil, None)) projectFileContents.append(newLine) // Write extracted directives to project.scala logger.message(s"Writing ${Constants.projectFileName}") os.write.over(inputs.workspace / Constants.projectFileName, projectFileContents.toString) def isProjectFile(position: Option[Position.File]): Boolean = position.exists(_.path.contains(inputs.workspace / Constants.projectFileName)) // Remove directives from their original files, skip the project.scala file directivesFromWritableMainInputs .filterNot(e => isProjectFile(e.position)) .foreach(d => removeDirectivesFrom(d.position)) directivesFromWritableTestInputs .filterNot(ttd => isProjectFile(ttd.positions)) .foreach(ttd => removeDirectivesFrom( position = ttd.positions, toKeep = ttd.noTestPrefixAvailable.filterNot(_.existsTestEquivalent) ) ) } private def getProjectSources(inputs: Inputs, logger: Logger)(using ScalaCliInvokeData ): Either[::[BuildException], (Sources, Sources)] = { val buildOptions = BuildOptions() val (crossSources, _) = CrossSources.forInputs( inputs, preprocessors = Sources.defaultPreprocessors( buildOptions.archiveCache, buildOptions.internal.javaClassNameVersionOpt, () => buildOptions.javaHome().value.javaCommand ), logger = logger, suppressWarningOptions = SuppressWarningOptions.suppressAll, exclude = buildOptions.internal.exclude, download = buildOptions.downloader ).orExit(logger) val sharedOptions = crossSources.sharedOptions(buildOptions) val scopedSources = crossSources.scopedSources(sharedOptions).orExit(logger) val mainSources = scopedSources.sources(Scope.Main, sharedOptions, inputs.workspace, logger) val testSources = scopedSources.sources(Scope.Test, sharedOptions, inputs.workspace, logger) (mainSources, testSources).traverseN } private def getExtractedDirectives( sources: Sources, suppressWarningOptions: SuppressWarningOptions )( using loggingUtilities: LoggingUtilities ): Seq[ExtractedDirectives] = { val logger = loggingUtilities.logger val fromPaths = sources.paths.map { (path, _) => val (_, content, _) = SheBang.partitionOnShebangSection(os.read(path)) logger.debug(s"Extracting directives from ${loggingUtilities.relativePath(path)}") ExtractedDirectives.from( contentChars = content.toCharArray, path = Right(path), suppressWarningOptions = suppressWarningOptions, logger = logger, maybeRecoverOnError = _ => None ).orExit(logger) } val fromInMemory = sources.inMemory.map { inMem => val originOrPath = inMem.originalPath.map((_, path) => path) val content = originOrPath match { case Right(path) => logger.debug(s"Extracting directives from ${loggingUtilities.relativePath(path)}") os.read(path) case Left(origin) => logger.debug(s"Extracting directives from $origin") inMem.wrapperParamsOpt match { // In case of script snippets, we need to drop the top wrapper lines case Some(wrapperParams) => String(inMem.content) .linesWithSeparators .drop(wrapperParams.topWrapperLineCount) .mkString case None => String(inMem.content) } } val (_, contentWithNoShebang, _) = SheBang.partitionOnShebangSection(content) ExtractedDirectives.from( contentChars = contentWithNoShebang.toCharArray, path = originOrPath, suppressWarningOptions = suppressWarningOptions, logger = logger, maybeRecoverOnError = _ => None ).orExit(logger) } fromPaths ++ fromInMemory } private def hasTargetDirectives(extractedDirectives: ExtractedDirectives): Boolean = { // Filter out all elements that contain using target directives val directivesInElement = extractedDirectives.directives.map(_.key) directivesInElement.exists(key => targetDirectivesKeysSet.contains(key)) } private def unifyCorrespondingNameAliases(extractedDirectives: Seq[ExtractedDirectives]) = extractedDirectives.map { extracted => // All keys that we migrate, not all in general val allKeysGrouped = usingDirectivesKeysGrouped ++ usingDirectivesWithTestPrefixKeysGrouped val strictDirectives = extracted.directives val strictDirectivesWithNewKeys = strictDirectives.flatMap { strictDir => val newKeyOpt = allKeysGrouped.find(_.nameAliases.contains(strictDir.key)) .flatMap(_.nameAliases.headOption) .map { key => if (key.startsWith("test")) val withTestStripped = key.stripPrefix("test").stripPrefix(".") "test." + withTestStripped.take(1).toLowerCase + withTestStripped.drop(1) else key } newKeyOpt.map(newKey => strictDir.copy(key = newKey)) } extracted.copy(directives = strictDirectivesWithNewKeys) } /** Transforms directives into their 'test.' equivalent if it exists * * @param extractedDirectives * @return * an instance of TransformedTestDirectives containing transformed directives and those that * could not be transformed since they have no 'test.' equivalent */ private def maybeTransformIntoTestEquivalent(extractedDirectives: Seq[ExtractedDirectives]) : Seq[TransformedTestDirectives] = for { extractedFromSingleElement <- extractedDirectives directives = extractedFromSingleElement.directives } yield { val (withInitialTestPrefix, noInitialTestPrefix) = directives.partition(_.hasTestPrefix) val (withTestEquivalent, noTestEquivalent) = noInitialTestPrefix.partition(_.existsTestEquivalent) val transformedToTestEquivalents = withTestEquivalent.map { case StrictDirective(key, values, _, _) => StrictDirective("test." + key, values) } TransformedTestDirectives( withTestPrefix = transformedToTestEquivalents ++ withInitialTestPrefix, noTestPrefixAvailable = noTestEquivalent, positions = extractedFromSingleElement.position ) } private def removeDirectivesFrom( position: Option[Position.File], toKeep: Seq[StrictDirective] = Nil )( using loggingUtilities: LoggingUtilities ): Unit = { position match { case Some(Position.File(Right(path), _, _, offset)) => val (shebangSection, strippedContent, newLine) = SheBang.partitionOnShebangSection(os.read(path)) def ignoreOrAddNewLine(str: String) = if str.isBlank then "" else str + newLine val keepLines = ignoreOrAddNewLine(shebangSection) + ignoreOrAddNewLine(toKeep.mkString( "", newLine, newLine )) val newContents = keepLines + strippedContent.drop(offset).stripLeading() val relativePath = loggingUtilities.relativePath(path) loggingUtilities.logger.message(s"Removing directives from $relativePath") if (toKeep.nonEmpty) { loggingUtilities.logger.message(" Keeping:") toKeep.foreach(d => loggingUtilities.logger.message(s" $d")) } os.write.over(path, newContents.stripLeading()) case _ => () } } private def createFormattedLinesAndAppend( strictDirectives: Seq[StrictDirective], projectFileContents: StringBuilder, isTest: Boolean ): Unit = { if (strictDirectives.nonEmpty) { projectFileContents .append(if (projectFileContents.nonEmpty) newLine else "") .append(if isTest then "// Test" else "// Main") .append(newLine) strictDirectives // group by key to merge values .groupBy(_.key) .map { (key, directives) => StrictDirective(key, directives.flatMap(_.values)) } // group by key prefixes to create splits between groups .groupBy(dir => (if (isTest) dir.key.stripPrefix(directiveTestPrefix) else dir.key).takeWhile(_ != '.') ) .map { (_, directives) => directives.flatMap(_.explodeToStringsWithColLimit()).toSeq.sorted } .toSeq .filter(_.nonEmpty) .sortBy(_.head)(using directivesOrdering) // append groups to the StringBuilder, add new lines between groups that are bigger than one line .foldLeft(0) { (lastSize, directiveLines) => val newSize = directiveLines.size if (lastSize > 1 || (lastSize != 0 && newSize > 1)) projectFileContents.append(newLine) directiveLines.foreach(projectFileContents.append(_).append(newLine)) newSize } } } private case class TransformedTestDirectives( withTestPrefix: Seq[StrictDirective], noTestPrefixAvailable: Seq[StrictDirective], positions: Option[Position.File] ) private case class LoggingUtilities( logger: Logger, workspacePath: os.Path ) { def relativePath(path: os.Path): FilePath & BasePathImpl = if (path.startsWith(workspacePath)) path.relativeTo(workspacePath) else path } private val directivesOrdering: Ordering[String] = { def directivesOrder(key: String): Int = { val handlersOrder = Seq( ScalaVersion.handler.keys, Platform.handler.keys, Jvm.handler.keys, JavaHome.handler.keys, ScalaNative.handler.keys, ScalaJs.handler.keys, ScalacOptions.handler.keys, JavaOptions.handler.keys, JavacOptions.handler.keys, JavaProps.handler.keys, MainClass.handler.keys, scala.build.preprocessing.directives.Sources.handler.keys, ObjectWrapper.handler.keys, Toolkit.handler.keys, Dependency.handler.keys ) handlersOrder.zipWithIndex .find(_._1.flatMap(_.nameAliases).contains(key)) .map(_._2) .getOrElse(if key.startsWith("publish") then 20 else 15) } Ordering.by { directiveLine => val key = directiveLine .stripPrefix("//> using") .stripLeading() .stripPrefix("test.") // separate key from value .takeWhile(!_.isWhitespace) directivesOrder(key) } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/fix/Fix.scala ================================================ package scala.cli.commands.fix import caseapp.core.RemainingArgs import scala.build.EitherCps.{either, value} import scala.build.{BuildThreads, Logger} import scala.cli.commands.ScalaCommand import scala.cli.commands.shared.SharedOptions import scala.cli.config.Keys import scala.cli.util.ConfigDbUtils object Fix extends ScalaCommand[FixOptions] { override def group = "Main" override def scalaSpecificationLevel = SpecificationLevel.EXPERIMENTAL override def sharedOptions(options: FixOptions): Option[SharedOptions] = Some(options.shared) override def runCommand(options: FixOptions, args: RemainingArgs, logger: Logger): Unit = { if options.areAnyRulesEnabled then { val inputs = options.shared.inputs(args.all).orExit(logger) val buildOpts = buildOptionsOrExit(options) val configDb = ConfigDbUtils.configDb.orExit(logger) if options.enableBuiltInRules then { logger.message("Running built-in rules...") if options.check then // TODO support --check for built-in rules: https://github.com/VirtusLab/scala-cli/issues/3423 logger.message("Skipping, '--check' is not yet supported for built-in rules.") else { BuiltInRules.runRules( inputs = inputs, buildOptions = buildOpts, logger = logger ) logger.message("Built-in rules completed.") } } if options.enableScalafix then either { logger.message("Running scalafix rules...") val threads = BuildThreads.create() val compilerMaker = options.shared.compilerMaker(threads) val workspace: os.Path = if args.all.isEmpty then os.pwd else inputs.workspace val actionableDiagnosticsEnabled = options.shared.logging.verbosityOptions.actions .orElse(configDb.get(Keys.actions).getOrElse(None)) val scalafixExitCode: Int = value { ScalafixRules.runRules( buildOptions = buildOpts, scalafixOptions = options.scalafix, sharedOptions = options.shared, inputs = inputs, check = options.check, compilerMaker = compilerMaker, actionableDiagnostics = actionableDiagnosticsEnabled, workspace = workspace, logger = logger ) } if scalafixExitCode != 1 then logger.message("scalafix rules completed.") else logger.error("scalafix rules failed.") sys.exit(scalafixExitCode) } } else logger.message("No rules were enabled. Did you disable everything intentionally?") } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/fix/FixOptions.scala ================================================ package scala.cli.commands.fix import caseapp.* import caseapp.core.help.Help import scala.cli.ScalaCli.fullRunnerName import scala.cli.commands.shared.{HasSharedOptions, HelpGroup, HelpMessages, SharedOptions} import scala.cli.commands.tags @HelpMessage(FixOptions.helpMessage, "", FixOptions.detailedHelpMessage) final case class FixOptions( @Recurse shared: SharedOptions = SharedOptions(), @Recurse scalafix: ScalafixOptions = ScalafixOptions(), @Group(HelpGroup.Fix.toString) @Tag(tags.experimental) @HelpMessage("Fail the invocation if rewrites are needed") @Tag(tags.inShortHelp) check: Boolean = false, @Group(HelpGroup.Fix.toString) @Tag(tags.experimental) @HelpMessage("Enable running Scalafix rules (enabled by default)") @Tag(tags.inShortHelp) @Name("scalafix") enableScalafix: Boolean = true, @Group(HelpGroup.Fix.toString) @Tag(tags.experimental) @HelpMessage("Enable running built-in rules (enabled by default)") @Tag(tags.inShortHelp) @Name("enableBuiltIn") @Name("builtIn") @Name("builtInRules") enableBuiltInRules: Boolean = true ) extends HasSharedOptions { def areAnyRulesEnabled: Boolean = enableScalafix || enableBuiltInRules } object FixOptions { implicit lazy val parser: Parser[FixOptions] = Parser.derive implicit lazy val help: Help[FixOptions] = Help.derive val cmdName = "fix" private val helpHeader = s"Run $fullRunnerName & Scalafix rules to lint, rewrite or otherwise rearrange a $fullRunnerName project." val helpMessage: String = HelpMessages.shortHelpMessage(cmdName, helpHeader) val detailedHelpMessage: String = s"""$helpHeader | |`scalafix` is used to check project code or rewrite it under the hood with use of specified rules. | |All standard $fullRunnerName inputs are accepted, but only Scala sources will be refactored (.scala and .sc files). | |${HelpMessages.commandConfigurations(cmdName)} | |${HelpMessages.acceptedInputs} | |${HelpMessages.commandDocWebsiteReference(cmdName)}""".stripMargin } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/fix/ScalafixOptions.scala ================================================ package scala.cli.commands.fix import caseapp.* import scala.cli.commands.shared.HelpGroup import scala.cli.commands.tags final case class ScalafixOptions( @Group(HelpGroup.Fix.toString) @Tag(tags.experimental) @HelpMessage("Custom path to the scalafix configuration file.") @Tag(tags.inShortHelp) scalafixConf: Option[String] = None, @Group(HelpGroup.Fix.toString) @Tag(tags.experimental) @HelpMessage("Pass extra argument(s) to scalafix.") @Tag(tags.inShortHelp) scalafixArg: List[String] = Nil, @Group(HelpGroup.Fix.toString) @Tag(tags.experimental) @HelpMessage("Run scalafix rule(s) explicitly, overriding the configuration file default.") @Tag(tags.inShortHelp) scalafixRules: List[String] = Nil ) object ScalafixOptions { implicit lazy val parser: Parser[ScalafixOptions] = Parser.derive implicit lazy val help: Help[ScalafixOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/fix/ScalafixRules.scala ================================================ package scala.cli.commands.fix import coursier.cache.FileCache import scala.build.EitherCps.{either, value} import scala.build.compiler.ScalaCompilerMaker import scala.build.errors.BuildException import scala.build.input.{Inputs, ScalaCliInvokeData} import scala.build.internal.{Constants, Runner} import scala.build.internals.ConsoleUtils.ScalaCliConsole.warnPrefix import scala.build.options.BuildOptions import scala.build.{Build, Logger, ScalafixArtifacts} import scala.cli.commands.shared.SharedOptions import scala.cli.commands.util.BuildCommandHelpers.copyOutput import scala.cli.commands.util.CommandHelpers object ScalafixRules extends CommandHelpers { def runRules( buildOptions: BuildOptions, scalafixOptions: ScalafixOptions, sharedOptions: SharedOptions, inputs: Inputs, compilerMaker: ScalaCompilerMaker, workspace: os.Path, check: Boolean, actionableDiagnostics: Option[Boolean], logger: Logger )(using ScalaCliInvokeData): Either[BuildException, Int] = { sharedOptions.semanticDbOptions.semanticDb match { case Some(false) => logger.message( s"""$warnPrefix SemanticDB files' generation was explicitly set to false. |$warnPrefix Some scalafix rules require .semanticdb files and may not work properly.""" .stripMargin ) case Some(true) => logger.debug("SemanticDB files' generation enabled.") case None => logger.debug("Defaulting SemanticDB files' generation to true, to satisfy scalafix needs.") } val buildOptionsWithSemanticDb = if buildOptions.scalaOptions.semanticDbOptions.generateSemanticDbs.isEmpty then buildOptions.copy(scalaOptions = buildOptions.scalaOptions.copy(semanticDbOptions = buildOptions.scalaOptions.semanticDbOptions.copy(generateSemanticDbs = Some(true) ) ) ) else buildOptions val shouldBuildTestScope = sharedOptions.scope.test.getOrElse(true) if !shouldBuildTestScope then logger.message( s"""$warnPrefix Building test scope was explicitly disabled. |$warnPrefix Some scalafix rules may not work correctly with test scope inputs.""" .stripMargin ) val res = Build.build( inputs, buildOptionsWithSemanticDb, compilerMaker, None, logger, crossBuilds = false, buildTests = shouldBuildTestScope, partial = None, actionableDiagnostics = actionableDiagnostics ) val builds = res.orExit(logger) builds.builds match case b if b.forall(_.success) => val successfulBuilds = b.collect { case s: Build.Successful => s } successfulBuilds.foreach(_.copyOutput(sharedOptions)) val classPaths = successfulBuilds.flatMap(_.fullClassPath).distinct val scalacOptions = successfulBuilds.headOption.toSeq .flatMap(_.options.scalaOptions.scalacOptions.toSeq.map(_.value.value)) val scalaVersion = { for { b <- successfulBuilds.headOption scalaParams <- b.scalaParams } yield scalaParams.scalaVersion }.getOrElse(Constants.defaultScalaVersion) either { val artifacts = value( ScalafixArtifacts.artifacts( scalaVersion, successfulBuilds.headOption.toSeq .flatMap(_.options.classPathOptions.scalafixDependencies.values.flatten), value(buildOptions.finalRepositories), logger, buildOptions.internal.cache.getOrElse(FileCache()) ) ) val scalafixCliOptions = scalafixOptions.scalafixConf.toList.flatMap(scalafixConf => List("--config", scalafixConf) ) ++ Seq("--sourceroot", workspace.toString) ++ Seq("--classpath", classPaths.mkString(java.io.File.pathSeparator)) ++ Seq("--scala-version", scalaVersion) ++ (if check then Seq("--test") else Nil) ++ (if scalacOptions.nonEmpty then scalacOptions.flatMap(Seq("--scalac-options", _)) else Nil) ++ (if artifacts.toolsJars.nonEmpty then Seq("--tool-classpath", artifacts.toolsJars.mkString(java.io.File.pathSeparator)) else Nil) ++ scalafixOptions.scalafixRules.flatMap(Seq("-r", _)) ++ scalafixOptions.scalafixArg val proc = Runner.runJvm( buildOptions.javaHome().value.javaCommand, buildOptions.javaOptions.javaOpts.toSeq.map(_.value.value), artifacts.scalafixJars, "scalafix.cli.Cli", scalafixCliOptions, logger, cwd = Some(workspace), allowExecve = true ) proc.waitFor() } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/fmt/Fmt.scala ================================================ package scala.cli.commands.fmt import caseapp.* import caseapp.core.help.HelpFormat import dependency.* import scala.build.Logger import scala.build.input.{ProjectScalaFile, SbtFile, Script, SourceScalaFile} import scala.build.internal.{Constants, ExternalBinaryParams, FetchExternalBinary, Runner} import scala.build.internals.ConsoleUtils.ScalaCliConsole.warnPrefix import scala.cli.CurrentParams import scala.cli.commands.ScalaCommand import scala.cli.commands.fmt.FmtUtil.* import scala.cli.commands.shared.{HelpCommandGroup, HelpGroup, SharedOptions} import scala.cli.util.ArgHelpers.* object Fmt extends ScalaCommand[FmtOptions] { override def group: String = HelpCommandGroup.Main.toString override def sharedOptions(options: FmtOptions): Option[SharedOptions] = Some(options.shared) override def scalaSpecificationLevel = SpecificationLevel.SHOULD val hiddenHelpGroups: Seq[HelpGroup] = Seq( HelpGroup.Scala, HelpGroup.Java, HelpGroup.Dependency, HelpGroup.ScalaJs, HelpGroup.ScalaNative, HelpGroup.CompilationServer, HelpGroup.Debug ) override def helpFormat: HelpFormat = super.helpFormat .withHiddenGroups(hiddenHelpGroups) .withHiddenGroupsWhenShowHidden(hiddenHelpGroups) .withPrimaryGroup(HelpGroup.Format) override def names: List[List[String]] = List( List("fmt"), List("format"), List("scalafmt") ) override def runCommand(options: FmtOptions, args: RemainingArgs, logger: Logger): Unit = { val buildOptions = buildOptionsOrExit(options) if options.shared.scope.test.nonEmpty then logger.message( s"""$warnPrefix Including the test scope does not change the behaviour of this command. |$warnPrefix Test scope inputs are formatted, regardless.""".stripMargin ) // TODO If no input is given, just pass '.' to scalafmt? val (sourceFiles, workspace, _) = if args.all.isEmpty then (Seq(os.pwd), os.pwd, None) else { val i = options.shared.inputs(args.all).orExit(logger) type FormattableSourceFile = Script | SourceScalaFile | ProjectScalaFile | SbtFile val s = i.sourceFiles().collect { case sc: FormattableSourceFile => sc.path } (s, i.workspace, Some(i)) } CurrentParams.workspaceOpt = Some(workspace) val (versionMaybe, dialectMaybe, pathMaybe) = readVersionAndDialect(workspace, options, logger) val cache = buildOptions.archiveCache if (sourceFiles.isEmpty) logger.debug("No source files, not formatting anything") else { val version = options.scalafmtVersion.getOrElse(versionMaybe.getOrElse(Constants.defaultScalafmtVersion)) val dialectString = options.scalafmtDialect.orElse(dialectMaybe).getOrElse { options.buildOptions.orExit(logger).scalaParams.orExit(logger).map(_.scalaVersion) .getOrElse(Constants.defaultScalaVersion) match case v if v.startsWith("2.11.") => "scala211" case v if v.startsWith("2.12.") => "scala212" case v if v.startsWith("2.13.") => "scala213" case v if v.startsWith("3.") => "scala3" case _ => "default" } val entry = { val dialect = ScalafmtDialect.fromString(dialectString) val prevConfMaybe = pathMaybe.map(p => os.read(p)) scalafmtConfigWithFields(prevConfMaybe.getOrElse(""), Some(version), dialect) } val scalaFmtConfPath = { val confFileName = ".scalafmt.conf" val path = if (options.saveScalafmtConf) pathMaybe.getOrElse(workspace / confFileName) else workspace / Constants.workspaceDirName / confFileName os.write.over(path, entry, createFolders = true) path } val fmtCommand = options.scalafmtLauncher.filter(_.nonEmpty) match { case Some(launcher) => Seq(launcher) case None => val (url, changing) = options.binaryUrl(version) val params = ExternalBinaryParams( url, changing, "scalafmt", Seq(dep"${Constants.scalafmtOrganization}:${Constants.scalafmtName}:$version"), "org.scalafmt.cli.Cli" ) FetchExternalBinary.fetch( params, cache, logger, () => buildOptions.javaHome().value.javaCommand ) .orExit(logger) .command } logger.debug(s"Launching scalafmt with command $fmtCommand") val command = fmtCommand ++ sourceFiles.map(_.toString) ++ options.scalafmtCliOptions ++ Seq("--config", scalaFmtConfPath.toString) val process = Runner.maybeExec( "scalafmt", command, logger, cwd = Some(workspace) ) sys.exit(process.waitFor()) } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/fmt/FmtOptions.scala ================================================ package scala.cli.commands.fmt import caseapp.* import scala.build.coursierVersion import scala.build.errors.BuildException import scala.build.internal.FetchExternalBinary import scala.build.options.BuildOptions import scala.cli.ScalaCli.fullRunnerName import scala.cli.commands.shared.{HasSharedOptions, HelpGroup, HelpMessages, SharedOptions} import scala.cli.commands.{Constants, tags} import scala.util.Properties // format: off @HelpMessage(FmtOptions.helpMessage, "", FmtOptions.detailedHelpMessage) final case class FmtOptions( @Recurse shared: SharedOptions = SharedOptions(), @Group(HelpGroup.Format.toString) @Tag(tags.should) @Tag(tags.inShortHelp) @HelpMessage("Check if sources are well formatted") check: Boolean = false, @Group(HelpGroup.Format.toString) @Tag(tags.implementation) @HelpMessage("Use project filters defined in the configuration. Turned on by default, use `--respect-project-filters:false` to disable it.") respectProjectFilters: Boolean = true, @Group(HelpGroup.Format.toString) @Tag(tags.implementation) @Tag(tags.inShortHelp) @HelpMessage("Saves .scalafmt.conf file if it was created or overwritten") saveScalafmtConf: Boolean = false, @Group(HelpGroup.Format.toString) @Tag(tags.implementation) @Hidden osArchSuffix: Option[String] = None, @Group(HelpGroup.Format.toString) @Tag(tags.implementation) @Hidden scalafmtTag: Option[String] = None, @Group(HelpGroup.Format.toString) @Tag(tags.implementation) @Hidden scalafmtGithubOrgName: Option[String] = None, @Group(HelpGroup.Format.toString) @Tag(tags.implementation) @Hidden scalafmtExtension: Option[String] = None, @Group(HelpGroup.Format.toString) @Tag(tags.implementation) @Hidden scalafmtLauncher: Option[String] = None, @Group(HelpGroup.Format.toString) @Name("F") @Tag(tags.implementation) @HelpMessage("Pass an argument to scalafmt.") @Tag(tags.inShortHelp) scalafmtArg: List[String] = Nil, @Group(HelpGroup.Format.toString) @HelpMessage("Custom path to the scalafmt configuration file.") @Tag(tags.implementation) @Tag(tags.inShortHelp) @Name("scalafmtConfig") scalafmtConf: Option[String] = None, @Group(HelpGroup.Format.toString) @Tag(tags.implementation) @HelpMessage("Pass configuration as a string.") @Name("scalafmtConfigStr") @Name("scalafmtConfSnippet") scalafmtConfStr: Option[String] = None, @Tag(tags.implementation) @Group(HelpGroup.Format.toString) @HelpMessage("Pass a global dialect for scalafmt. This overrides whatever value is configured in the .scalafmt.conf file or inferred based on Scala version used.") @Tag(tags.implementation) @Name("dialect") @Tag(tags.inShortHelp) scalafmtDialect: Option[String] = None, @Tag(tags.implementation) @Group(HelpGroup.Format.toString) @HelpMessage(s"Pass scalafmt version before running it (${Constants.defaultScalafmtVersion} by default). If passed, this overrides whatever value is configured in the .scalafmt.conf file.") @Name("fmtVersion") @Tag(tags.inShortHelp) scalafmtVersion: Option[String] = None ) extends HasSharedOptions { // format: on def binaryUrl(version: String): (String, Boolean) = { val osArchSuffix0 = osArchSuffix.map(_.trim).filter(_.nonEmpty) .getOrElse(FetchExternalBinary.platformSuffix()) val tag0 = scalafmtTag.getOrElse("v" + version) val gitHubOrgName0 = scalafmtGithubOrgName.getOrElse { version.coursierVersion match { case v if v < "3.5.9".coursierVersion => "scala-cli/scalafmt-native-image" // since version 3.5.9 scalafmt-native-image repository was moved to VirtusLab organisation case v if v < "3.9.1".coursierVersion => "virtuslab/scalafmt-native-image" // since version 3.9.1 native images for all platforms are provided by ScalaMeta case _ => "scalameta/scalafmt" } } val extension0 = version match { case v if v.coursierVersion >= "3.9.1".coursierVersion || Properties.isWin => ".zip" case _ => ".gz" } val url = s"https://github.com/$gitHubOrgName0/releases/download/$tag0/scalafmt-$osArchSuffix0$extension0" (url, !tag0.startsWith("v")) } def buildOptions: Either[BuildException, BuildOptions] = shared.buildOptions() def scalafmtCliOptions: List[String] = scalafmtArg ::: (if (check && !scalafmtArg.contains("--check")) List("--check") else Nil) ::: (if (respectProjectFilters && !scalafmtArg.contains("--respect-project-filters")) List("--respect-project-filters") else Nil) } object FmtOptions { implicit lazy val parser: Parser[FmtOptions] = Parser.derive implicit lazy val help: Help[FmtOptions] = Help.derive val cmdName = "fmt" private val helpHeader = "Formats Scala code." val helpMessage: String = HelpMessages.shortHelpMessage(cmdName, helpHeader) val detailedHelpMessage: String = s"""$helpHeader | |`scalafmt` is used to perform the formatting under the hood. | |The `.scalafmt.conf` configuration file is optional. |Default configuration values will be assumed by $fullRunnerName. | |All standard $fullRunnerName inputs are accepted, but only Scala sources will be formatted (.scala and .sc files). | |${HelpMessages.commandDocWebsiteReference(cmdName)}""".stripMargin } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/fmt/FmtUtil.scala ================================================ package scala.cli.commands.fmt import com.typesafe.config.parser.{ConfigDocument, ConfigDocumentFactory} import com.typesafe.config.{ConfigParseOptions, ConfigSyntax} import scala.build.Logger import scala.build.internal.Constants import scala.util.control.NonFatal object FmtUtil { private def getGitRoot(workspace: os.Path, logger: Logger): Option[String] = try { val result = os.proc("git", "rev-parse", "--show-toplevel").call( cwd = workspace, stderr = os.ProcessOutput.ReadBytes((_, _) => ()) ).out.trim() Option(result) } catch { case NonFatal(e) => logger.log( s"""Could not get root of the git repository. |Cause: $e""".stripMargin ) None } /** Based on scalafmt * [comment](https://github.com/scalameta/scalafmt/blob/d0c11e98898334969f5f4dfc4bd511630cf00ab9/scalafmt-cli/src/main/scala/org/scalafmt/cli/CliArgParser.scala). * First we look for .scalafmt.conf file in the `cwd`. If not found we go to the `git root` and * look there. * * @return * path to found `.scalafmt.conf` file and `version` with `dialect` read from it */ def readVersionAndDialect( workspace: os.Path, options: FmtOptions, logger: Logger ): (Option[String], Option[String], Option[os.Path]) = { case class RunnerMetaconfig(dialect: String = "") object RunnerMetaconfig { lazy val default: RunnerMetaconfig = RunnerMetaconfig("") implicit lazy val surface: metaconfig.generic.Surface[RunnerMetaconfig] = metaconfig.generic.deriveSurface[RunnerMetaconfig] implicit lazy val decoder: metaconfig.ConfDecoder[RunnerMetaconfig] = metaconfig.generic.deriveDecoder[RunnerMetaconfig](default) } case class ScalafmtMetaconfig( version: String = "", runner: RunnerMetaconfig = RunnerMetaconfig("") ) object ScalafmtMetaconfig { lazy val default: ScalafmtMetaconfig = ScalafmtMetaconfig() implicit lazy val surface: metaconfig.generic.Surface[ScalafmtMetaconfig] = metaconfig.generic.deriveSurface[ScalafmtMetaconfig] implicit lazy val decoder: metaconfig.ConfDecoder[ScalafmtMetaconfig] = metaconfig.generic.deriveDecoder[ScalafmtMetaconfig](default) } val confName = ".scalafmt.conf" val pathMaybe = options.scalafmtConfStr.flatMap { s => val tmpConfPath = workspace / Constants.workspaceDirName / ".scalafmt.conf" os.write.over(tmpConfPath, s, createFolders = true) Some(tmpConfPath) }.orElse { options.scalafmtConf.flatMap { p => val confPath = os.Path(p, os.pwd) logger.debug(s"Checking for $confPath.") if (os.exists(confPath)) Some(confPath) else logger.message(s"WARNING: provided file doesn't exist $confPath") None }.orElse { logger.debug(s"Checking for $confName in cwd.") val confInCwd = workspace / confName if (os.exists(confInCwd)) Some(confInCwd) else { logger.debug(s"Checking for $confName in git root.") val gitRootMaybe = getGitRoot(workspace, logger) val confInGitRootMaybe = gitRootMaybe.map(os.Path(_) / confName) confInGitRootMaybe.find(os.exists(_)) } } } val confContentMaybe = pathMaybe.flatMap { path => val either = metaconfig.Hocon.parseInput[ScalafmtMetaconfig]( metaconfig.Input.File(path.toNIO) ).toEither either.left.foreach(confErr => logger.log(confErr.toString())) either.toOption } val versionMaybe = confContentMaybe.collect { case conf if conf.version.trim.nonEmpty => conf.version } val dialectMaybe = confContentMaybe.collect { case conf if conf.runner.dialect.trim.nonEmpty => conf.runner.dialect } (versionMaybe, dialectMaybe, pathMaybe) } // Based on https://github.com/scalameta/metals/blob/main/metals/src/main/scala/scala/meta/internal/metals/ScalafmtDialect.scala sealed abstract class ScalafmtDialect(val value: String) object ScalafmtDialect { case object Scala3 extends ScalafmtDialect("scala3") case object Scala213 extends ScalafmtDialect("scala213") case object Scala213Source3 extends ScalafmtDialect("scala213source3") case object Scala212 extends ScalafmtDialect("scala212") case object Scala212Source3 extends ScalafmtDialect("scala212source3") case object Scala211 extends ScalafmtDialect("scala211") implicit val ord: Ordering[ScalafmtDialect] = new Ordering[ScalafmtDialect] { override def compare(x: ScalafmtDialect, y: ScalafmtDialect): Int = prio(x) - prio(y) private def prio(d: ScalafmtDialect): Int = d match { case Scala211 => 1 case Scala212 => 2 case Scala212Source3 => 3 case Scala213 => 4 case Scala213Source3 => 5 case Scala3 => 6 } } def fromString(v: String): Option[ScalafmtDialect] = v.toLowerCase match { case "default" => Some(Scala213) case "scala211" => Some(Scala211) case "scala212" => Some(Scala212) case "scala212source3" => Some(Scala212Source3) case "scala213" => Some(Scala213) case "scala213source3" => Some(Scala213Source3) case "scala3" => Some(Scala3) case _ => None } } /** Based on scalameta [fmt * config](https://github.com/scalameta/metals/blob/main/metals/src/main/scala/scala/meta/internal/metals/ScalafmtConfig.scala) * * @return * Scalafmt configuration content based on previousConfigText with updated fields */ def scalafmtConfigWithFields( previousConfigText: String, version: Option[String] = None, runnerDialect: Option[ScalafmtDialect] = None, fileOverride: Map[String, ScalafmtDialect] = Map.empty ): String = { def docFrom(s: String): ConfigDocument = { val options = ConfigParseOptions.defaults().setSyntax(ConfigSyntax.CONF) ConfigDocumentFactory.parseString(s, options) } def withUpdatedVersion(content: String, v: String): String = { val doc = docFrom(content) if (doc.hasPath("version")) doc.withValueText("version", s"\"$v\"").render else { // prepend to the beggining of file val sb = new StringBuilder sb.append(s"""version = "$v"""") sb.append(System.lineSeparator) sb.append(content) sb.toString } } def withUpdatedDialect(content: String, d: ScalafmtDialect): String = { val doc = docFrom(content) if (doc.hasPath("runner.dialect")) doc.withValueText("runner.dialect", d.value).render else { // append to the end val sb = new StringBuilder sb.append(content) val sep = System.lineSeparator val lastLn = content.endsWith(sep) if (!lastLn) sb.append(sep) sb.append(s"runner.dialect = ${d.value}") sb.append(sep) sb.toString } } def withFileOverride( content: String, overrides: Map[String, ScalafmtDialect] ): String = if (overrides.isEmpty) content else { val sep = System.lineSeparator val values = overrides .map { case (key, dialect) => s"""| "$key" { | runner.dialect = ${dialect.value} | }""".stripMargin } .mkString(s"fileOverride {$sep", sep, s"$sep}$sep") val addSep = if (content.endsWith(sep)) "" else sep content + addSep + values } val doNothing = identity[String] val combined = List( version.fold(doNothing)(v => withUpdatedVersion(_, v)), runnerDialect.fold(doNothing)(v => withUpdatedDialect(_, v)), withFileOverride(_, fileOverride) ).reduceLeft(_ andThen _) combined(previousConfigText) } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/github/GitHubApi.scala ================================================ package scala.cli.commands.github import com.github.plokhotnyuk.jsoniter_scala.core.* import com.github.plokhotnyuk.jsoniter_scala.macros.* import java.util.Base64 object GitHubApi { final case class Secret( name: String, created_at: String, updated_at: String ) final case class SecretList( total_count: Int, secrets: List[Secret] ) implicit val secretListCodec: JsonValueCodec[SecretList] = JsonCodecMaker.make final case class PublicKey( key_id: String, key: String ) { def decodedKey: Array[Byte] = Base64.getDecoder().decode(key) } implicit val publicKeyCodec: JsonValueCodec[PublicKey] = JsonCodecMaker.make final case class EncryptedSecret( encrypted_value: String, key_id: String ) implicit val encryptedSecretCodec: JsonValueCodec[EncryptedSecret] = JsonCodecMaker.make } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/github/HasSharedSecretOptions.scala ================================================ package scala.cli.commands.github import scala.cli.commands.shared.{GlobalOptions, HasGlobalOptions} trait HasSharedSecretOptions extends HasGlobalOptions { def shared: SharedSecretOptions override def global: GlobalOptions = shared.global } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/github/LibSodiumJni.scala ================================================ package scala.cli.commands.github import coursier.cache.{ArchiveCache, Cache, CacheLogger} import coursier.core.Type import coursier.util.Task import java.io.InputStream import java.nio.charset.StandardCharsets import java.util.Locale import scala.build.EitherCps.{either, value} import scala.build.Logger import scala.build.errors.BuildException import scala.build.internal.{Constants, FetchExternalBinary} import scala.build.internals.EnvVar import scala.cli.internal.Constants as CliConstants import scala.util.Properties import scala.util.control.NonFatal object LibSodiumJni { private def isGraalvmNativeImage: Boolean = sys.props.contains("org.graalvm.nativeimage.imagecode") private def launcherKindOpt = { val path = CliConstants.launcherTypeResourcePath val resUrl = Thread.currentThread.getContextClassLoader.getResource(path) if (resUrl == null) None else { var is: InputStream = null try { is = resUrl.openStream() Some(new String(resUrl.openStream().readAllBytes(), StandardCharsets.UTF_8)) } finally if (is != null) is.close() } } private def condaLibsodiumVersion = Constants.condaLibsodiumVersion private def alpineLibsodiumVersion = Constants.alpineLibsodiumVersion private def libsodiumjniVersion = Constants.libsodiumjniVersion private def alpineVersion = Constants.alpineVersion private def archiveUrlAndPath() = if (Properties.isLinux && launcherKindOpt.contains("static")) // Should actually be unused, as we statically link libsodium from the static launcher // Keeping it just-in-case. This could be useful from a musl-based JVM. ( s"https://dl-cdn.alpinelinux.org/alpine/v$alpineVersion/main/x86_64/libsodium-$alpineLibsodiumVersion-r1.apk", os.rel / "usr" / "lib" / "libsodium.so.23.3.0" // FIXME Could this change? ) else { val condaPlatform = FetchExternalBinary.condaPlatform // FIXME These suffixes seem to be hashes, and seem to change at every version… // We'd need a way to find them automatically. val suffix = condaPlatform match { case "linux-64" => "-h36c2ea0_1" case "linux-aarch64" => "-hb9de7d4_1" case "osx-64" => "-hbcb3906_1" case "osx-arm64" => "-h27ca646_1" case "win-64" => "-h62dcd97_1" case other => sys.error(s"Unrecognized conda platform $other") } val relPath = condaPlatform match { case "linux-64" => os.rel / "lib" / "libsodium.so" case "linux-aarch64" => os.rel / "lib" / "libsodium.so" case "osx-64" => os.rel / "lib" / "libsodium.dylib" case "osx-arm64" => os.rel / "lib" / "libsodium.dylib" case "win-64" => os.rel / "Library" / "bin" / "libsodium.dll" case other => sys.error(s"Unrecognized conda platform $other") } ( s"https://anaconda.org/conda-forge/libsodium/$condaLibsodiumVersion/download/$condaPlatform/libsodium-$condaLibsodiumVersion$suffix.tar.bz2", relPath ) } private def jniLibArtifact(cache: Cache[Task]) = { import dependency._ import scala.build.internal.Util.DependencyOps val classifier = FetchExternalBinary.platformSuffix(supportsMusl = false) val ext = if (Properties.isLinux) "so" else if (Properties.isMac) "dylib" else if (Properties.isWin) "dll" else sys.error(s"Unrecognized operating system: ${sys.props("os.name")}") val dep = dep"org.virtuslab.scala-cli:libsodiumjni:$libsodiumjniVersion,intransitive,classifier=$classifier,ext=$ext,type=$ext" val fetch = coursier.Fetch() .addDependencies(dep.toCs) .addArtifactTypes(Type(ext)) val files = cache.loggerOpt.getOrElse(CacheLogger.nop).use { try fetch.run() catch { case NonFatal(e) => throw new Exception(e) } } files match { case Seq() => sys.error(s"Cannot find $dep") case Seq(file) => file case other => sys.error(s"Unexpectedly got too many files while resolving $dep: $other") } } def init( cache: Cache[Task], archiveCache: ArchiveCache[Task], logger: Logger ): Either[BuildException, Unit] = either { val allStaticallyLinked = Properties.isWin && isGraalvmNativeImage || Properties.isLinux && launcherKindOpt.contains("static") if (!allStaticallyLinked) { val (archiveUrl, pathInArchive) = archiveUrlAndPath() val sodiumLibOpt = value { FetchExternalBinary.fetchLauncher( archiveUrl, changing = false, archiveCache, logger, "", launcherPathOpt = Some(pathInArchive), makeExecutable = false ) } val f = jniLibArtifact(cache) sodiumLibOpt match { case Some(sodiumLib) => System.load(sodiumLib.toString) case None => val allow = EnvVar.ScalaCli.allowSodiumJni.valueOpt .map(_.toLowerCase(Locale.ROOT)) .forall { case "false" | "0" => false case _ => true } if (allow) System.loadLibrary("sodium") else value(Left(new LibSodiumNotFound(archiveUrl))) } libsodiumjni.internal.LoadLibrary.initialize(f.toString) } libsodiumjni.Sodium.init() } final class LibSodiumNotFound(url: String) extends BuildException(s"libsodium: $url not found") } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/github/SecretCreate.scala ================================================ package scala.cli.commands.github import caseapp.core.RemainingArgs import caseapp.core.help.HelpFormat import com.github.plokhotnyuk.jsoniter_scala.core.* import coursier.cache.ArchiveCache import sttp.client3.* import java.nio.charset.StandardCharsets import java.util.Base64 import scala.build.EitherCps.{either, value} import scala.build.Logger import scala.cli.commands.publish.ConfigUtil.* import scala.cli.commands.shared.HelpGroup import scala.cli.commands.util.ScalaCliSttpBackend import scala.cli.commands.{ScalaCommand, SpecificationLevel} import scala.cli.config.{PasswordOption, Secret} import scala.cli.errors.GitHubApiError import scala.cli.util.ArgHelpers.* object SecretCreate extends ScalaCommand[SecretCreateOptions] { override def scalaSpecificationLevel: SpecificationLevel = SpecificationLevel.EXPERIMENTAL override def helpFormat: HelpFormat = super.helpFormat.withPrimaryGroup(HelpGroup.Secret) override def names = List( List("github", "secret", "create"), List("gh", "secret", "create") ) private def parseSecretKv(input: String): (String, Secret[String]) = input.split("=", 2) match { case Array(name, value) => PasswordOption.parse(value) match { case Left(err) => sys.error(s"Error parsing secret: $err") case Right(opt) => name -> opt.get() } case _ => sys.error( s"Malformed secret '$input' (expected name=password, with password either file:path, command:command, or value:value)" ) } def publicKey( repoOrg: String, repoName: String, token: Secret[String], backend: SttpBackend[Identity, Any], logger: Logger ): Either[GitHubApiError, GitHubApi.PublicKey] = either { // https://docs.github.com/en/rest/reference/actions#get-a-repository-public-key val publicKeyResp = basicRequest .get(uri"https://api.github.com/repos/$repoOrg/$repoName/actions/secrets/public-key") .header("Authorization", s"token ${token.value}") .header("Accept", "application/vnd.github.v3+json") .send(backend) if (publicKeyResp.code.code != 200) value(Left(new GitHubApiError( s"Error getting public key (code ${publicKeyResp.code}) for $repoOrg/$repoName" ))) val publicKeyRespBody = publicKeyResp.body match { case Left(_) => // should not happen if response code is 200? value(Left(new GitHubApiError( s"Unexpected missing body in response when listing secrets of $repoOrg/$repoName" ))) case Right(value) => value } logger.debug(s"Public key: $publicKeyRespBody") readFromString(publicKeyRespBody)(using GitHubApi.publicKeyCodec) } def createOrUpdate( repoOrg: String, repoName: String, token: Secret[String], secretName: String, secretValue: Secret[String], pubKey: GitHubApi.PublicKey, dummy: Boolean, printRequest: Boolean, backend: SttpBackend[Identity, Any], logger: Logger ): Either[GitHubApiError, Boolean] = either { val secretBytes = secretValue.value.getBytes(StandardCharsets.UTF_8) val encryptedValue = libsodiumjni.Sodium.seal(secretBytes, pubKey.decodedKey) val content = GitHubApi.EncryptedSecret( encrypted_value = Base64.getEncoder().encodeToString(encryptedValue), key_id = pubKey.key_id ) // https://docs.github.com/en/rest/reference/actions#create-or-update-a-repository-secret val uri = uri"https://api.github.com/repos/$repoOrg/$repoName/actions/secrets/$secretName" val requestBody = writeToArray(content)(using GitHubApi.encryptedSecretCodec) if (printRequest) System.out.write(requestBody) if (dummy) { logger.debug(s"Dummy mode - would have sent a request to $uri") logger.message( s"Dummy mode - NOT uploading secret $secretName to $repoOrg/$repoName" ) false } else { val r = basicRequest .put(uri) .header("Authorization", s"token ${token.value}") .header("Accept", "application/vnd.github.v3+json") .body(requestBody) .send(backend) r.code.code match { case 201 => logger.message(s" created $secretName") true case 204 => logger.message(s" updated $secretName") false case code => value(Left(new GitHubApiError( s"Unexpected status code $code in response when creating secret $secretName in $repoOrg/$repoName" ))) } } } override def runCommand( options: SecretCreateOptions, args: RemainingArgs, logger: Logger ): Unit = { val secrets = args.all.map(parseSecretKv) val backend = ScalaCliSttpBackend.httpURLConnection(logger) val pubKey = options.publicKey.filter(_.trim.nonEmpty) match { case Some(path) => val content = os.read.bytes(os.Path(path, os.pwd)) readFromArray(content)(using GitHubApi.publicKeyCodec) case None => publicKey( options.shared.repoOrg, options.shared.repoName, options.shared.token.get().toConfig, backend, logger ).orExit(logger) } val cache = options.coursier.coursierCache(logger) val archiveCache = ArchiveCache().withCache(cache) LibSodiumJni.init(cache, archiveCache, logger) for ((name, secretValue) <- secrets) { logger.debug(s"Secret name: $name") createOrUpdate( options.shared.repoOrg, options.shared.repoName, options.shared.token.get().toConfig, name, secretValue, pubKey, options.dummy, options.printRequest, backend, logger ).orExit(logger) } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/github/SecretCreateOptions.scala ================================================ package scala.cli.commands.github import caseapp.* import scala.cli.ScalaCli.progName import scala.cli.commands.shared.{CoursierOptions, HelpGroup} import scala.cli.commands.tags // format: off @HelpMessage( s"""Creates or updates a GitHub repository secret. | ${Console.BOLD}$progName --power github secret create --repo repo-org/repo-name SECRET_VALUE=value:secret${Console.RESET}""".stripMargin ) final case class SecretCreateOptions( @Recurse shared: SharedSecretOptions = SharedSecretOptions(), @Recurse coursier: CoursierOptions = CoursierOptions(), @Group(HelpGroup.Secret.toString) @Tag(tags.restricted) @Tag(tags.inShortHelp) @ExtraName("pubKey") publicKey: Option[String] = None, @Tag(tags.implementation) @ExtraName("n") dummy: Boolean = false, @Hidden @Tag(tags.implementation) @Group(HelpGroup.Secret.toString) printRequest: Boolean = false ) extends HasSharedSecretOptions // format: on object SecretCreateOptions { implicit lazy val parser: Parser[SecretCreateOptions] = Parser.derive implicit lazy val help: Help[SecretCreateOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/github/SecretList.scala ================================================ package scala.cli.commands.github import caseapp.core.RemainingArgs import caseapp.core.help.HelpFormat import com.github.plokhotnyuk.jsoniter_scala.core.* import sttp.client3.* import scala.build.EitherCps.{either, value} import scala.build.Logger import scala.cli.commands.publish.ConfigUtil.* import scala.cli.commands.shared.HelpGroup import scala.cli.commands.util.ScalaCliSttpBackend import scala.cli.commands.{ScalaCommand, SpecificationLevel} import scala.cli.config.Secret import scala.cli.errors.GitHubApiError import scala.cli.util.ArgHelpers.* object SecretList extends ScalaCommand[SecretListOptions] { override def scalaSpecificationLevel: SpecificationLevel = SpecificationLevel.EXPERIMENTAL override def helpFormat: HelpFormat = super.helpFormat.withPrimaryGroup(HelpGroup.Secret) override def names = List( List("github", "secret", "list"), List("gh", "secret", "list") ) def list( repoOrg: String, repoName: String, token: Secret[String], backend: SttpBackend[Identity, Any], logger: Logger ): Either[GitHubApiError, GitHubApi.SecretList] = either { // https://docs.github.com/en/rest/reference/actions#list-repository-secrets val uri = uri"https://api.github.com/repos/$repoOrg/$repoName/actions/secrets" logger.debug(s"Listing secrets: attempting request: $uri") val r = basicRequest .get(uri) .header("Authorization", s"token ${token.value}") .header("Accept", "application/vnd.github.v3+json") .send(backend) if r.code.code != 200 then value { Left(new GitHubApiError( s"Unexpected status code ${r.code.code} in response when listing secrets of $repoOrg/$repoName" )) } else logger.debug("Listing secrets: request successful") // FIXME Paging val body = r.body match { case Left(_) => // should not happen if response code is 200? value(Left(new GitHubApiError( s"Unexpected missing body in response when listing secrets of $repoOrg/$repoName" ))) case Right(value) => value } readFromString(body)(using GitHubApi.secretListCodec) } override def runCommand( options: SecretListOptions, args: RemainingArgs, logger: Logger ): Unit = { val backend = ScalaCliSttpBackend.httpURLConnection(logger) val list0 = list( options.shared.repoOrg, options.shared.repoName, options.shared.token.get().toConfig, backend, logger ).orExit(logger) // FIXME Paging System.err.println(s"Found ${list0.total_count} secret(s)") for (s <- list0.secrets) println(s.name) } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/github/SecretListOptions.scala ================================================ package scala.cli.commands.github import caseapp.* // format: off @HelpMessage("Lists secrets for a given GitHub repository.") final case class SecretListOptions( @Recurse shared: SharedSecretOptions ) extends HasSharedSecretOptions // format: on object SecretListOptions { implicit lazy val parser: Parser[SecretListOptions] = Parser.derive implicit lazy val help: Help[SecretListOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/github/SharedSecretOptions.scala ================================================ package scala.cli.commands.github import caseapp.* import scala.cli.commands.shared.{GlobalOptions, HasGlobalOptions, HelpGroup} import scala.cli.commands.tags import scala.cli.signing.shared.{PasswordOption, Secret} import scala.cli.signing.util.ArgParsers.* // format: off final case class SharedSecretOptions( @Recurse global: GlobalOptions = GlobalOptions(), @Group(HelpGroup.Secret.toString) @Tag(tags.restricted) @Tag(tags.inShortHelp) token: PasswordOption = PasswordOption.Value(Secret("")), @ExtraName("repo") @Group(HelpGroup.Secret.toString) @Tag(tags.restricted) @Tag(tags.inShortHelp) repository: String = "" ) extends HasGlobalOptions { // format: on lazy val (repoOrg, repoName) = repository.split('/') match { case Array(org, name) => (org, name) case _ => sys.error(s"Malformed repository: '$repository' (expected 'org/name')") } } object SharedSecretOptions { implicit lazy val parser: Parser[SharedSecretOptions] = Parser.derive implicit lazy val help: Help[SharedSecretOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/installcompletions/InstallCompletions.scala ================================================ package scala.cli.commands.installcompletions import caseapp.* import caseapp.core.complete.{Bash, Fish, Zsh} import caseapp.core.help.HelpFormat import java.nio.charset.Charset import java.nio.file.Paths import java.util import scala.build.internals.EnvVar import scala.build.{Directories, Logger} import scala.cli.ScalaCli import scala.cli.commands.shared.HelpGroup import scala.cli.commands.{ScalaCommand, SpecificationLevel} import scala.cli.internal.ProfileFileUpdater import scala.cli.util.ArgHelpers.* object InstallCompletions extends ScalaCommand[InstallCompletionsOptions] { override def names = List( List("install", "completions"), List("install-completions") ) override def helpFormat: HelpFormat = super.helpFormat.withPrimaryGroup(HelpGroup.Install) override def scalaSpecificationLevel: SpecificationLevel = SpecificationLevel.IMPLEMENTATION override def runCommand( options: InstallCompletionsOptions, args: RemainingArgs, logger: Logger ): Unit = { val interactive = options.global.logging.verbosityOptions.interactiveInstance() lazy val completionsDir = options.output .map(os.Path(_, os.pwd)) .getOrElse(Directories.directories.completionsDir) val name = getName(options.name) val format = getFormat(options.format).getOrElse { val msg = "Cannot determine current shell. Which would you like to use?" interactive.chooseOne(msg, List("zsh", "bash")).getOrElse { System.err.println( "Cannot determine current shell, pass the shell you use with --shell, like" ) System.err.println(s"$name install completions --shell zsh") System.err.println(s"$name install completions --shell bash") System.err.println(s"$name install completions --shell fish") sys.exit(1) } } val (rcScript, defaultRcFile) = format match { case Bash.id | "bash" => val script = Bash.script(name) val defaultRcFile = os.home / ".bashrc" (script, defaultRcFile) case Zsh.id | "zsh" => val completionScript = Zsh.script(name) val zDotDir = EnvVar.Misc.zDotDir.valueOpt .map(os.Path(_, os.pwd)) .getOrElse(os.home) val defaultRcFile = zDotDir / ".zshrc" val dir = completionsDir / "zsh" val completionScriptDest = dir / s"_$name" val content = completionScript.getBytes(Charset.defaultCharset()) val needsWrite = !os.exists(completionScriptDest) || !util.Arrays.equals(os.read.bytes(completionScriptDest), content) if (needsWrite) { logger.log(s"Writing $completionScriptDest") os.write.over(completionScriptDest, content, createFolders = true) } val script = Seq( s"""fpath=("$dir" $$fpath)""", "compinit" ).map(_ + System.lineSeparator()).mkString (script, defaultRcFile) case Fish.id | "fish" => val script = Fish.script(name) val defaultRcFile = os.home / ".config" / "fish" / "config.fish" (script, defaultRcFile) case _ => System.err.println(s"Unrecognized or unsupported shell: $format") sys.exit(1) } if (options.env) println(rcScript) else { val rcFile = options.rcFile.map(os.Path(_, os.pwd)).getOrElse(defaultRcFile) val banner = options.banner.replace("{NAME}", name) val updated = ProfileFileUpdater.addToProfileFile( rcFile.toNIO, banner, rcScript, Charset.defaultCharset() ) if (options.global.logging.verbosity >= 0) if (updated) { System.err.println(s"Updated $rcFile") System.err.println( s"It is recommended to reload your shell, or source $rcFile in the " + "current session, for its changes to be taken into account." ) } else System.err.println(s"$rcFile already up-to-date") } } def getName(name: Option[String]): String = name.getOrElse { val progName = ScalaCli.progName Paths.get(progName).getFileName.toString } def getFormat(format: Option[String]): Option[String] = format.map(_.trim).filter(_.nonEmpty) .orElse { EnvVar.Misc.shell.valueOpt.map(_.split("[\\/]+").last).map { case "bash" => Bash.id case "zsh" => Zsh.id case "fish" => Fish.id case other => other } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/installcompletions/InstallCompletionsOptions.scala ================================================ package scala.cli.commands.installcompletions import caseapp.* import scala.cli.ScalaCli.fullRunnerName import scala.cli.commands.shared.{GlobalOptions, HasGlobalOptions, HelpGroup, HelpMessages} import scala.cli.commands.tags // format: off @HelpMessage(InstallCompletionsOptions.helpMessage, "", InstallCompletionsOptions.detailedHelpMessage) final case class InstallCompletionsOptions( @Recurse global: GlobalOptions = GlobalOptions(), @Group(HelpGroup.Install.toString) @Name("shell") @Tag(tags.implementation) @Tag(tags.inShortHelp) @HelpMessage("Name of the shell, either zsh or bash") format: Option[String] = None, @Tag(tags.implementation) @Group(HelpGroup.Install.toString) @Tag(tags.inShortHelp) @HelpMessage("Path to `*rc` file, defaults to `.bashrc` or `.zshrc` depending on shell") rcFile: Option[String] = None, @Tag(tags.implementation) @HelpMessage("Completions output directory") @Group(HelpGroup.Install.toString) @Name("o") output: Option[String] = None, @Hidden @Tag(tags.implementation) @HelpMessage("Custom banner in comment placed in rc file") @Group(HelpGroup.Install.toString) banner: String = "{NAME} completions", @Hidden @Tag(tags.implementation) @HelpMessage("Custom completions name") @Group(HelpGroup.Install.toString) name: Option[String] = None, @Tag(tags.implementation) @HelpMessage("Print completions to stdout") @Group(HelpGroup.Install.toString) env: Boolean = false, ) extends HasGlobalOptions // format: on object InstallCompletionsOptions { implicit lazy val parser: Parser[InstallCompletionsOptions] = Parser.derive implicit lazy val help: Help[InstallCompletionsOptions] = Help.derive private val helpHeader = s"Installs $fullRunnerName completions into your shell" val helpMessage: String = s"""$helpHeader | |${HelpMessages.commandFullHelpReference("install completions")} |${HelpMessages.commandDocWebsiteReference("completions")}""".stripMargin val detailedHelpMessage: String = s"""$helpHeader | |${HelpMessages.commandDocWebsiteReference("completions")}""".stripMargin } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/installhome/InstallHome.scala ================================================ package scala.cli.commands.installhome import caseapp.* import caseapp.core.help.HelpFormat import coursier.env.{EnvironmentUpdate, ProfileUpdater} import scala.build.Logger import scala.cli.commands.shared.HelpGroup import scala.cli.commands.{ CommandUtils, CustomWindowsEnvVarUpdater, ScalaCommand, SpecificationLevel } import scala.cli.util.ArgHelpers.* import scala.io.StdIn.readLine import scala.util.Properties object InstallHome extends ScalaCommand[InstallHomeOptions] { override def hidden: Boolean = true override def scalaSpecificationLevel: SpecificationLevel = SpecificationLevel.IMPLEMENTATION override def helpFormat: HelpFormat = super.helpFormat.withPrimaryGroup(HelpGroup.Install) private def logEqual(version: String, logger: Logger) = { logger.message(s"$fullRunnerName $version is already installed and up-to-date.") sys.exit(0) } private def logUpdate( env: Boolean, newVersion: String, oldVersion: String, logger: Logger ): Unit = if (!env) logger.message( s"""$baseRunnerName $oldVersion is already installed and out-of-date. |$baseRunnerName will be updated to version $newVersion |""".stripMargin ) private def logDowngrade( env: Boolean, newVersion: String, oldVersion: String, logger: Logger ): Unit = if (!env && coursier.paths.Util.useAnsiOutput()) { logger.message( s"$baseRunnerName $oldVersion is already installed and up-to-date." ) logger.error( s"Do you want to downgrade $baseRunnerName to version $newVersion [Y/n]" ) val response = readLine() if (response != "Y") { logger.message("Abort") sys.exit(1) } } else { logger.error( s"Error: $baseRunnerName is already installed $oldVersion and up-to-date. Downgrade to $newVersion pass -f or --force." ) sys.exit(1) } override def runCommand( options: InstallHomeOptions, args: RemainingArgs, logger: Logger ): Unit = { val binDirPath = options.binDirPath.getOrElse( scala.build.Directories.default().binRepoDir / baseRunnerName ) val destBinPath = binDirPath / options.binaryName val newScalaCliBinPath = os.Path(options.scalaCliBinaryPath, os.pwd) val newVersion: String = os.proc(newScalaCliBinPath, "version", "--cli-version").call(cwd = os.pwd).out.trim() // Backward compatibility - previous versions not have the `--version` parameter val oldVersion: String = if (os.isFile(destBinPath)) { val res = os.proc(destBinPath, "version", "--cli-version").call(cwd = os.pwd, check = false) if (res.exitCode == 0) res.out.trim() else "0.0.0" } else "0.0.0" if (os.exists(binDirPath)) if (options.force) () // skip logging else if (newVersion == oldVersion) logEqual(newVersion, logger) else if (CommandUtils.isOutOfDateVersion(newVersion, oldVersion)) logUpdate(options.env, newVersion, oldVersion, logger) else logDowngrade(options.env, newVersion, oldVersion, logger) if (os.exists(destBinPath)) os.remove(destBinPath) os.copy( from = newScalaCliBinPath, to = destBinPath, createFolders = true ) if (!Properties.isWin) os.perms.set(destBinPath, os.PermSet.fromString("rwxr-xr-x")) if (options.env) println(s"""export PATH="$binDirPath:$$PATH"""") else { val update = EnvironmentUpdate(Nil, Seq("PATH" -> binDirPath.toString())) val didUpdate = if (Properties.isWin) { val updater = CustomWindowsEnvVarUpdater().withUseJni(Some(coursier.paths.Util.useJni())) updater.applyUpdate(update) } else { val updater = ProfileUpdater() updater.applyUpdate(update) } println(s"Successfully installed $baseRunnerName $newVersion") if (didUpdate) { if (Properties.isLinux) println( s"""|Profile file(s) updated. |To run $baseRunnerName, log out and log back in, or run 'source ~/.profile'""".stripMargin ) if (Properties.isMac) println( s"To run $baseRunnerName, open new terminal or run 'source ~/.profile'" ) } } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/installhome/InstallHomeOptions.scala ================================================ package scala.cli.commands.installhome import caseapp.* import scala.cli.ScalaCli.{baseRunnerName, fullRunnerName} import scala.cli.commands.shared.{GlobalOptions, HasGlobalOptions, HelpGroup} import scala.cli.commands.tags // format: off @HelpMessage(s"Install $fullRunnerName in a sub-directory of the home directory") final case class InstallHomeOptions( @Recurse global: GlobalOptions = GlobalOptions(), @Group(HelpGroup.Install.toString) @Tag(tags.implementation) scalaCliBinaryPath: String, @Group(HelpGroup.Install.toString) @Name("f") @Tag(tags.implementation) @HelpMessage("Overwrite if it exists") force: Boolean = false, @Group(HelpGroup.Install.toString) @Hidden @Tag(tags.implementation) @HelpMessage("Binary name") binaryName: String = baseRunnerName, @Group(HelpGroup.Install.toString) @Tag(tags.implementation) @HelpMessage("Print the update to `env` variable") env: Boolean = false, @Group(HelpGroup.Install.toString) @Hidden @Tag(tags.implementation) @HelpMessage("Binary directory") binDir: Option[String] = None ) extends HasGlobalOptions { // format: on lazy val binDirPath = binDir.map(os.Path(_, os.pwd)) } object InstallHomeOptions { implicit lazy val parser: Parser[InstallHomeOptions] = Parser.derive implicit lazy val help: Help[InstallHomeOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/new/New.scala ================================================ package scala.cli.commands.`new` import caseapp.core.RemainingArgs import dependency.* import scala.build.EitherCps.{either, value} import scala.build.internal.Util.safeFullDetailedArtifacts import scala.build.internal.{Constants, OsLibc, Runner} import scala.build.options.{BuildOptions, JavaOptions} import scala.build.{Artifacts, Logger, Positioned} import scala.cli.commands.ScalaCommand import scala.cli.commands.shared.{CoursierOptions, HelpCommandGroup} object New extends ScalaCommand[NewOptions] { override def group: String = HelpCommandGroup.Main.toString override def scalaSpecificationLevel = SpecificationLevel.EXPERIMENTAL private def giter8Dependency = Seq(dep"${Constants.giter8Organization}::${Constants.giter8Name}:${Constants.giter8Version}") override def runCommand(options: NewOptions, remainingArgs: RemainingArgs, logger: Logger): Unit = either { val scalaParameters = ScalaParameters(Constants.defaultScala213Version) val fetchedGiter8 = Artifacts.fetchAnyDependencies( giter8Dependency.map(Positioned.none), Seq.empty, Some(scalaParameters), logger, CoursierOptions().coursierCache(logger), None ) match { case Right(value) => value case Left(value) => System.err.println(value.message) sys.exit(1) } val giter8 = value(fetchedGiter8.fullDetailedArtifacts0.safeFullDetailedArtifacts) .collect { case (_, _, _, Some(f)) => os.Path(f, os.pwd) } val buildOptions = BuildOptions( javaOptions = JavaOptions( jvmIdOpt = Some(OsLibc.defaultJvm(OsLibc.jvmIndexOs)).map(Positioned.none) ) ) val exitCode = Runner.runJvm( buildOptions.javaHome().value.javaCommand, buildOptions.javaOptions.javaOpts.toSeq.map(_.value.value), giter8, "giter8.Giter8", remainingArgs.remaining, logger, allowExecve = true ).waitFor() sys.exit(exitCode) } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/new/NewOptions.scala ================================================ package scala.cli.commands.`new` import caseapp.* import scala.cli.commands.shared.* @HelpMessage(NewOptions.newMessage, "", NewOptions.detailedNewMessage) final case class NewOptions( @Recurse global: GlobalOptions = GlobalOptions() ) extends HasGlobalOptions object NewOptions { implicit lazy val parser: Parser[NewOptions] = Parser.derive implicit lazy val help: Help[NewOptions] = Help.derive val cmdName = "new" private val newHeader = "New giter8 template." val newMessage: String = HelpMessages.shortHelpMessage(cmdName, newHeader) val detailedNewMessage: String = s"""$newHeader | | Creates a new project from a giter8 template. | |""".stripMargin } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala ================================================ package scala.cli.commands.package0 import caseapp.* import caseapp.core.help.HelpFormat import coursier.core import coursier.core.Resolution import coursier.launcher.* import dependency.* import os.{BasePathImpl, FilePath, Path, SegmentedPath} import packager.config.* import packager.deb.DebianPackage import packager.docker.DockerPackage import packager.mac.dmg.DmgPackage import packager.mac.pkg.PkgPackage import packager.rpm.RedHatPackage import packager.windows.WindowsPackage import java.io.{ByteArrayOutputStream, OutputStream} import java.nio.file.attribute.FileTime import java.util.zip.{ZipEntry, ZipOutputStream} import scala.build.* import scala.build.EitherCps.{either, value} import scala.build.Ops.* import scala.build.errors.* import scala.build.interactive.InteractiveFileOps import scala.build.internal.Util.* import scala.build.internal.resource.NativeResourceMapper import scala.build.internal.{Runner, ScalaJsLinkerConfig} import scala.build.options.PackageType.Native import scala.build.options.{BuildOptions, JavaOpt, PackageType, Platform, ScalaNativeTarget, Scope} import scala.cli.CurrentParams import scala.cli.commands.OptionsHelper.* import scala.cli.commands.doc.Doc import scala.cli.commands.packaging.Spark import scala.cli.commands.run.Run.{createPythonInstance, orPythonDetectionError} import scala.cli.commands.shared.{HelpCommandGroup, HelpGroup, MainClassOptions, SharedOptions} import scala.cli.commands.util.BuildCommandHelpers import scala.cli.commands.util.BuildCommandHelpers.* import scala.cli.commands.{CommandUtils, ScalaCommand, WatchUtil} import scala.cli.config.Keys import scala.cli.errors.ScalaJsLinkingError import scala.cli.internal.{CachedBinary, Constants, ProcUtil, ScalaJsLinker} import scala.cli.packaging.{Library, NativeImage} import scala.cli.util.ArgHelpers.* import scala.cli.util.ConfigDbUtils import scala.util.Properties object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers { override def name = "package" override def group: String = HelpCommandGroup.Main.toString val primaryHelpGroups: Seq[HelpGroup] = Seq( HelpGroup.Package, HelpGroup.Scala, HelpGroup.Java, HelpGroup.Debian, HelpGroup.MacOS, HelpGroup.RedHat, HelpGroup.Windows, HelpGroup.Docker, HelpGroup.NativeImage ) val hiddenHelpGroups: Seq[HelpGroup] = Seq(HelpGroup.Entrypoint, HelpGroup.Watch) override def helpFormat: HelpFormat = super.helpFormat .withHiddenGroups(hiddenHelpGroups) .withPrimaryGroups(primaryHelpGroups) override def sharedOptions(options: PackageOptions): Option[SharedOptions] = Some(options.shared) override def scalaSpecificationLevel = SpecificationLevel.RESTRICTED override def buildOptions(options: PackageOptions): Option[BuildOptions] = Some(options.baseBuildOptions(options.shared.logger).orExit(options.shared.logger)) override def runCommand(options: PackageOptions, args: RemainingArgs, logger: Logger): Unit = { val inputs = options.shared.inputs(args.remaining).orExit(logger) CurrentParams.workspaceOpt = Some(inputs.workspace) // FIXME mainClass encoding has issues with special chars, such as '-' val initialBuildOptions = finalBuildOptions(options) val threads = BuildThreads.create() val compilerMaker = options.compilerMaker(threads).orExit(logger) val docCompilerMakerOpt = options.docCompilerMakerOpt val cross = options.compileCross.cross.getOrElse(false) val configDb = ConfigDbUtils.configDb.orExit(logger) val actionableDiagnostics = options.shared.logging.verbosityOptions.actions.orElse( configDb.get(Keys.actions).getOrElse(None) ) val withTestScope = options.shared.scope.test.getOrElse(false) if options.watch.watchMode then { var expectedModifyEpochSecondOpt = Option.empty[Long] val watcher = Build.watch( inputs, initialBuildOptions, compilerMaker, docCompilerMakerOpt, logger, crossBuilds = cross, buildTests = withTestScope, partial = None, actionableDiagnostics = actionableDiagnostics, postAction = () => WatchUtil.printWatchMessage() ) { res => res.orReport(logger).map(_.all).foreach { case b if b.forall(_.success) => val successfulBuilds = b.collect { case s: Build.Successful => s } successfulBuilds.foreach(_.copyOutput(options.shared)) val mtimeDestPath = doPackageCrossBuilds( logger = logger, outputOpt = options.output.filter(_.nonEmpty), force = options.force, forcedPackageTypeOpt = options.forcedPackageTypeOpt, allBuilds = successfulBuilds, extraArgs = args.unparsed, expectedModifyEpochSecondOpt = expectedModifyEpochSecondOpt, allowTerminate = !options.watch.watchMode, mainClassOptions = options.mainClass, withTestScope = withTestScope ) .orReport(logger) for (valueOpt <- mtimeDestPath) expectedModifyEpochSecondOpt = valueOpt case b if b.exists(bb => !bb.success && !bb.cancelled) => System.err.println("Compilation failed") case _ => System.err.println("Build cancelled") } } try WatchUtil.waitForCtrlC(() => watcher.schedule()) finally watcher.dispose() } else Build.build( inputs, initialBuildOptions, compilerMaker, docCompilerMakerOpt, logger, crossBuilds = cross, buildTests = withTestScope, partial = None, actionableDiagnostics = actionableDiagnostics ) .orExit(logger) .all match { case b if b.forall(_.success) => val successfulBuilds = b.collect { case s: Build.Successful => s } successfulBuilds.foreach(_.copyOutput(options.shared)) val res0 = doPackageCrossBuilds( logger = logger, outputOpt = options.output.filter(_.nonEmpty), force = options.force, forcedPackageTypeOpt = options.forcedPackageTypeOpt, allBuilds = successfulBuilds, extraArgs = args.unparsed, expectedModifyEpochSecondOpt = None, allowTerminate = !options.watch.watchMode, mainClassOptions = options.mainClass, withTestScope = withTestScope ) res0.orExit(logger) case b if b.exists(bb => !bb.success && !bb.cancelled) => System.err.println("Compilation failed") sys.exit(1) case _ => System.err.println("Build cancelled") sys.exit(1) } } def finalBuildOptions(options: PackageOptions): BuildOptions = { val initialOptions = options.finalBuildOptions(options.shared.logger).orExit(options.shared.logger) val finalBuildOptions = initialOptions.copy(scalaOptions = initialOptions.scalaOptions.copy(defaultScalaVersion = Some(defaultScalaVersion)) ) val buildOptions = finalBuildOptions.copy( javaOptions = finalBuildOptions.javaOptions.copy( javaOpts = finalBuildOptions.javaOptions.javaOpts ++ options.java.allJavaOpts.map(JavaOpt(_)).map(Positioned.commandLine) ) ) buildOptions } private def insertSuffixBeforeExtension(name: String, suffix: String): String = if suffix.isEmpty then name else { val dotIdx = name.lastIndexOf('.') if dotIdx > 0 then name.substring(0, dotIdx) + suffix + name.substring(dotIdx) else name + suffix } private def doPackageCrossBuilds( logger: Logger, outputOpt: Option[String], force: Boolean, forcedPackageTypeOpt: Option[PackageType], allBuilds: Seq[Build.Successful], extraArgs: Seq[String], expectedModifyEpochSecondOpt: Option[Long], allowTerminate: Boolean, mainClassOptions: MainClassOptions, withTestScope: Boolean ): Either[BuildException, Option[Long]] = either { val crossBuildGroups = allBuilds.groupedByCrossParams.toSeq val multipleCrossGroups = crossBuildGroups.size > 1 if multipleCrossGroups then logger.message(s"Packaging ${crossBuildGroups.size} cross builds...") val platforms = crossBuildGroups.map(_._1.platform).distinct val needsPlatformInSuffix = platforms.size > 1 val results = value { crossBuildGroups.map { (crossParams, builds) => val crossSuffix = if multipleCrossGroups then { val versionPart = s"_${crossParams.scalaVersion}" if needsPlatformInSuffix then s"${versionPart}_${crossParams.platform}" else versionPart } else "" if multipleCrossGroups then logger.message(s"Packaging for ${crossParams.asString}...") doPackage( logger = logger, outputOpt = outputOpt, force = force, forcedPackageTypeOpt = forcedPackageTypeOpt, builds = builds, extraArgs = extraArgs, expectedModifyEpochSecondOpt = expectedModifyEpochSecondOpt, allowTerminate = allowTerminate, mainClassOptions = mainClassOptions, withTestScope = withTestScope, crossSuffix = crossSuffix ) } .sequence .left.map(CompositeBuildException(_)) } results.lastOption.flatten } private def doPackage( logger: Logger, outputOpt: Option[String], force: Boolean, forcedPackageTypeOpt: Option[PackageType], builds: Seq[Build.Successful], extraArgs: Seq[String], expectedModifyEpochSecondOpt: Option[Long], allowTerminate: Boolean, mainClassOptions: MainClassOptions, withTestScope: Boolean, crossSuffix: String ): Either[BuildException, Option[Long]] = either { if mainClassOptions.mainClassLs.contains(true) then value { mainClassOptions .maybePrintMainClasses( builds.flatMap(_.foundMainClasses()).distinct, shouldExit = allowTerminate ) .map(_ => None) } else { val packageType: PackageType = value(resolvePackageType(builds, forcedPackageTypeOpt)) // TODO When possible, call alreadyExistsCheck() before compiling stuff def extension = packageType match { case PackageType.LibraryJar => ".jar" case PackageType.SourceJar => ".jar" case PackageType.DocJar => ".jar" case _: PackageType.Assembly => ".jar" case PackageType.Spark => ".jar" case PackageType.Js => ".js" case PackageType.Debian => ".deb" case PackageType.Dmg => ".dmg" case PackageType.Pkg => ".pkg" case PackageType.Rpm => ".rpm" case PackageType.Msi => ".msi" case PackageType.Native.Application => if Properties.isWin then ".exe" else "" case PackageType.Native.LibraryDynamic => if Properties.isWin then ".dll" else if Properties.isMac then ".dylib" else ".so" case PackageType.Native.LibraryStatic => if Properties.isWin then ".lib" else ".a" case PackageType.GraalVMNativeImage if Properties.isWin => ".exe" case _ if Properties.isWin => ".bat" case _ => "" } def defaultName = packageType match { case PackageType.LibraryJar => "library.jar" case PackageType.SourceJar => "source.jar" case PackageType.DocJar => "scaladoc.jar" case _: PackageType.Assembly => "app.jar" case PackageType.Spark => "job.jar" case PackageType.Js => "app.js" case PackageType.Debian => "app.deb" case PackageType.Dmg => "app.dmg" case PackageType.Pkg => "app.pkg" case PackageType.Rpm => "app.rpm" case PackageType.Msi => "app.msi" case PackageType.Native.Application => if Properties.isWin then "app.exe" else "app" case PackageType.Native.LibraryDynamic => if Properties.isWin then "library.dll" else if Properties.isMac then "library.dylib" else "library.so" case PackageType.Native.LibraryStatic => if Properties.isWin then "library.lib" else "library.a" case PackageType.GraalVMNativeImage if Properties.isWin => "app.exe" case _ if Properties.isWin => "app.bat" case _ => "app" } val output = outputOpt.map { case path if packageType == PackageType.GraalVMNativeImage && Properties.isWin && !path.endsWith(".exe") => s"$path.exe" // graalvm-native-image requires .exe extension on Windows case path => path } val packageOutput = builds.head.options.notForBloopOptions.packageOptions.output val dest = output.orElse(packageOutput) .orElse { builds.flatMap(_.sources.defaultMainClass) .headOption .map(n => n.drop(n.lastIndexOf('.') + 1)) .map(_.stripSuffix("_sc")) .map(_ + extension) } .orElse { builds.flatMap(_.retainedMainClass(logger).toOption) .headOption .map(_.stripSuffix("_sc") + extension) } .orElse(builds.flatMap(_.sources.paths).collectFirst(_._1.baseName + extension)) .getOrElse(defaultName) val destPath = { val base = os.Path(dest, Os.pwd) if crossSuffix.nonEmpty then base / os.up / insertSuffixBeforeExtension(base.last, crossSuffix) else base } val printableDest = CommandUtils.printablePath(destPath) def alreadyExistsCheck(): Either[BuildException, Unit] = if !force && os.exists(destPath) && !expectedModifyEpochSecondOpt.contains(os.mtime(destPath)) then builds.head.options.interactive.map { interactive => InteractiveFileOps.erasingPath(interactive, printableDest, destPath) { () => val errorMsg = if expectedModifyEpochSecondOpt.isEmpty then s"$printableDest already exists" else s"$printableDest was overwritten by another process" System.err.println(s"Error: $errorMsg. Pass -f or --force to force erasing it.") sys.exit(1) } } else Right(()) value(alreadyExistsCheck()) def mainClass: Either[BuildException, String] = builds.head.options.mainClass.filter(_.nonEmpty) match { case Some(cls) => Right(cls) case None => val potentialMainClasses = builds.flatMap(_.foundMainClasses()).distinct builds .map { build => build.retainedMainClass(logger, potentialMainClasses) .map(mainClass => build.scope -> mainClass) } .sequence .left .map(CompositeBuildException(_)) .map(_.toMap) .map { retainedMainClassesByScope => if retainedMainClassesByScope.size == 1 then retainedMainClassesByScope.head._2 else retainedMainClassesByScope .get(Scope.Main) .orElse(retainedMainClassesByScope.get(Scope.Test)) .get } } def mainClassOpt: Option[String] = mainClass.toOption val packageOptions = builds.head.options.notForBloopOptions.packageOptions val outputPath = packageType match { case PackageType.Bootstrap => value(bootstrap(builds, destPath, value(mainClass), () => alreadyExistsCheck(), logger)) destPath case PackageType.LibraryJar => val libraryJar = Library.libraryJar(builds) value(alreadyExistsCheck()) if force then os.copy.over(libraryJar, destPath, createFolders = true) else os.copy(libraryJar, destPath, createFolders = true) destPath case PackageType.SourceJar => val now = System.currentTimeMillis() val content = sourceJar(builds, now) value(alreadyExistsCheck()) if force then os.write.over(destPath, content, createFolders = true) else os.write(destPath, content, createFolders = true) destPath case PackageType.DocJar => val docJarPath = value(docJar(builds, logger, extraArgs, withTestScope)) value(alreadyExistsCheck()) if force then os.copy.over(docJarPath, destPath, createFolders = true) else os.copy(docJarPath, destPath, createFolders = true) destPath case a: PackageType.Assembly => value { assembly( builds = builds, destPath = destPath, mainClassOpt = a.mainClassInManifest match { case None => if a.addPreamble then { val clsName = value { mainClass.left.map { case e: NoMainClassFoundError => // This one has a slightly better error message, suggesting --preamble=false new NoMainClassFoundForAssemblyError(e) case e => e } } Some(clsName) } else mainClassOpt case Some(false) => None case Some(true) => Some(value(mainClass)) }, extraProvided = Nil, withPreamble = a.addPreamble, alreadyExistsCheck = () => alreadyExistsCheck(), logger = logger ) } destPath case PackageType.Spark => value { assembly( builds, destPath, mainClassOpt, // The Spark modules are assumed to be already on the class path, // along with all their transitive dependencies (originating from // the Spark distribution), so we don't include any of them in the // assembly. Spark.sparkModules, withPreamble = false, () => alreadyExistsCheck(), logger ) } destPath case PackageType.Js => value(buildJs(builds, destPath, mainClassOpt, logger)) case tpe: PackageType.Native => import PackageType.Native.* val mainClassO = tpe match case Application => Some(value(mainClass)) case _ => None val cachedDest = value(buildNative( builds = builds, mainClass = mainClassO, targetType = tpe, destPath = Some(destPath), logger = logger )) if force then os.copy.over(cachedDest, destPath, createFolders = true) else os.copy(cachedDest, destPath, createFolders = true) destPath case PackageType.GraalVMNativeImage => NativeImage.buildNativeImage( builds, value(mainClass), destPath, builds.head.inputs.nativeImageWorkDir, extraArgs, logger ) destPath case nativePackagerType: PackageType.NativePackagerType => val bootstrapPath = os.temp.dir(prefix = "scala-packager") / "app" value { bootstrap( builds, bootstrapPath, value(mainClass), () => alreadyExistsCheck(), logger ) } val sharedSettings = SharedSettings( sourceAppPath = bootstrapPath, version = packageOptions.packageVersion, force = force, outputPath = destPath, logoPath = packageOptions.logoPath, launcherApp = packageOptions.launcherApp ) lazy val debianSettings = DebianSettings( shared = sharedSettings, maintainer = packageOptions.maintainer.mandatory("--maintainer", "debian"), description = packageOptions.description.mandatory("--description", "debian"), debianConflicts = packageOptions.debianOptions.conflicts, debianDependencies = packageOptions.debianOptions.dependencies, architecture = packageOptions.debianOptions.architecture.mandatory( "--deb-architecture", "debian" ), priority = packageOptions.debianOptions.priority, section = packageOptions.debianOptions.section ) lazy val macOSSettings = MacOSSettings( shared = sharedSettings, identifier = packageOptions.macOSidentifier.mandatory("--identifier-parameter", "macOs") ) lazy val redHatSettings = RedHatSettings( shared = sharedSettings, description = packageOptions.description.mandatory("--description", "redHat"), license = packageOptions.redHatOptions.license.mandatory("--license", "redHat"), release = packageOptions.redHatOptions.release.mandatory("--release", "redHat"), rpmArchitecture = packageOptions.redHatOptions.architecture.mandatory( "--rpm-architecture", "redHat" ) ) lazy val windowsSettings = WindowsSettings( shared = sharedSettings, maintainer = packageOptions.maintainer.mandatory("--maintainer", "windows"), licencePath = packageOptions.windowsOptions.licensePath.mandatory( "--licence-path", "windows" ), productName = packageOptions.windowsOptions.productName.mandatory( "--product-name", "windows" ), exitDialog = packageOptions.windowsOptions.exitDialog, suppressValidation = packageOptions.windowsOptions.suppressValidation.getOrElse(false), extraConfigs = packageOptions.windowsOptions.extraConfig, is64Bits = packageOptions.windowsOptions.is64Bits.getOrElse(true), installerVersion = packageOptions.windowsOptions.installerVersion, wixUpgradeCodeGuid = packageOptions.windowsOptions.wixUpgradeCodeGuid ) nativePackagerType match { case PackageType.Debian => DebianPackage(debianSettings).build() case PackageType.Dmg => DmgPackage(macOSSettings).build() case PackageType.Pkg => PkgPackage(macOSSettings).build() case PackageType.Rpm => RedHatPackage(redHatSettings).build() case PackageType.Msi => WindowsPackage(windowsSettings).build() } destPath case PackageType.Docker => value(docker(builds, value(mainClass), logger)) destPath } val printableOutput = CommandUtils.printablePath(outputPath) if packageType.runnable.nonEmpty then logger.message { if packageType.runnable.contains(true) then s"Wrote $outputPath, run it with" + System.lineSeparator() + " " + printableOutput else if packageType == PackageType.Js then s"Wrote $outputPath, run it with" + System.lineSeparator() + " node " + printableOutput else s"Wrote $outputPath" } val mTimeDestPathOpt = if packageType.runnable.isEmpty then None else Some(os.mtime(destPath)) mTimeDestPathOpt } // end of doPackage } def docJar( builds: Seq[Build.Successful], logger: Logger, extraArgs: Seq[String], withTestScope: Boolean ): Either[BuildException, os.Path] = either { val workDir = builds.head.inputs.docJarWorkDir val dest = workDir / "doc.jar" val cacheData = CachedBinary.getCacheData( builds = builds, config = extraArgs.toList, dest = dest, workDir = workDir ) if cacheData.changed then { val contentDir = value(Doc.generateScaladocDirPath(builds, logger, extraArgs, withTestScope)) var outputStream: OutputStream = null try { outputStream = os.write.outputStream(dest, createFolders = true) Library.writeLibraryJarTo( outputStream, builds, hasActualManifest = false, contentDirOverride = Some(contentDir) ) } finally if outputStream != null then outputStream.close() CachedBinary.updateProjectAndOutputSha(dest, workDir, cacheData.projectSha) } dest } private val generatedSourcesPrefix = os.rel / "META-INF" / "generated" def sourceJar(builds: Seq[Build.Successful], defaultLastModified: Long): Array[Byte] = { val baos = new ByteArrayOutputStream var zos: ZipOutputStream = null def fromSimpleSources = builds.flatMap(_.sources.paths).distinct.iterator.map { case (path, relPath) => val lastModified = os.mtime(path) val content = os.read.bytes(path) (relPath, content, lastModified) } def fromGeneratedSources = builds.flatMap(_.sources.inMemory).distinct.iterator.flatMap { inMemSource => val lastModified = inMemSource.originalPath match { case Right((_, origPath)) => os.mtime(origPath) case Left(_) => defaultLastModified } val originalOpt = inMemSource.originalPath.toOption.collect { case (subPath, origPath) if subPath != inMemSource.generatedRelPath => val origContent = os.read.bytes(origPath) (subPath, origContent, lastModified) } val prefix = if (originalOpt.isEmpty) os.rel else generatedSourcesPrefix val generated = ( prefix / inMemSource.generatedRelPath, inMemSource.content, lastModified ) Iterator(generated) ++ originalOpt.iterator } def paths: Iterator[(FilePath & BasePathImpl & SegmentedPath, Array[Byte], Long)] = fromSimpleSources ++ fromGeneratedSources try { zos = new ZipOutputStream(baos) for ((relPath, content, lastModified) <- paths) { val name = relPath.toString val ent = new ZipEntry(name) ent.setLastModifiedTime(FileTime.fromMillis(lastModified)) ent.setSize(content.length) zos.putNextEntry(ent) zos.write(content) zos.closeEntry() } } finally if zos != null then zos.close() baos.toByteArray } private def docker( builds: Seq[Build.Successful], mainClass: String, logger: Logger ): Either[BuildException, Unit] = either { val packageOptions = builds.head.options.notForBloopOptions.packageOptions if builds.head.options.platform.value == Platform.Native && (Properties.isMac || Properties.isWin) then { System.err.println( "Package scala native application to docker image is not supported on MacOs and Windows" ) sys.exit(1) } val exec = packageOptions.dockerOptions.cmd.orElse { builds.head.options.platform.value match { case Platform.JVM => Some("sh") case Platform.JS => Some("node") case Platform.Native => None } } val from = packageOptions.dockerOptions.from.getOrElse { builds.head.options.platform.value match { case Platform.JVM => "openjdk:17.0.2-slim" case Platform.JS => "node" case Platform.Native => "debian:stable-slim" } } val repository = packageOptions.dockerOptions.imageRepository.mandatory( "--docker-image-repository", "docker" ) val tag = packageOptions.dockerOptions.imageTag.getOrElse("latest") val dockerSettings = DockerSettings( from = from, registry = packageOptions.dockerOptions.imageRegistry, repository = repository, tag = Some(tag), exec = exec, dockerExecutable = None, extraDirectories = packageOptions.dockerOptions.extraDirectories.map(_.toNIO) ) val appPath = os.temp.dir(prefix = "scala-cli-docker") / "app" builds.head.options.platform.value match { case Platform.JVM => value(bootstrap(builds, appPath, mainClass, () => Right(()), logger)) case Platform.JS => buildJs(builds, appPath, Some(mainClass), logger) case Platform.Native => val dest = value(buildNative( builds = builds, mainClass = Some(mainClass), targetType = PackageType.Native.Application, destPath = None, logger = logger )) os.copy(dest, appPath) } logger.message("Started building docker image with your application, it might take some time") DockerPackage(appPath, dockerSettings).build() logger.message( "Built docker image, run it with" + System.lineSeparator() + s" docker run $repository:$tag" ) } private def buildJs( builds: Seq[Build.Successful], destPath: os.Path, mainClass: Option[String], logger: Logger ): Either[BuildException, os.Path] = for { isFullOpt <- builds.head.options.scalaJsOptions.fullOpt linkerConfig = builds.head.options.scalaJsOptions.linkerConfig(logger) linkResult <- linkJs( builds = builds, dest = destPath, mainClassOpt = mainClass, addTestInitializer = false, config = linkerConfig, fullOpt = isFullOpt, noOpt = builds.head.options.scalaJsOptions.noOpt.getOrElse(false), logger = logger ) } yield linkResult private def bootstrap( builds: Seq[Build.Successful], destPath: os.Path, mainClass: String, alreadyExistsCheck: () => Either[BuildException, Unit], logger: Logger ): Either[BuildException, Unit] = either { val byteCodeZipEntries = builds.flatMap { build => os.walk(build.output) .filter(os.isFile(_)) .map { path => val name = path.relativeTo(build.output).toString val content = os.read.bytes(path) val lastModified = os.mtime(path) val entry = new ZipEntry(name) entry.setLastModifiedTime(FileTime.fromMillis(lastModified)) entry.setSize(content.length) (entry, content) } } // TODO Generate that in memory val tmpJar = os.temp(prefix = destPath.last.stripSuffix(".jar"), suffix = ".jar") val tmpJarParams = Parameters.Assembly() .withExtraZipEntries(byteCodeZipEntries) .withMainClass(mainClass) AssemblyGenerator.generate(tmpJarParams, tmpJar.toNIO) val tmpJarContent = os.read.bytes(tmpJar) os.remove(tmpJar) def dependencyEntries: Seq[ClassPathEntry] = builds.flatMap(_.artifacts.artifacts).distinct.map { case (url, path) => if builds.head.options.notForBloopOptions.packageOptions.isStandalone then ClassPathEntry.Resource(path.last, os.mtime(path), os.read.bytes(path)) else ClassPathEntry.Url(url) } val byteCodeEntry = ClassPathEntry.Resource(s"${destPath.last}-content.jar", 0L, tmpJarContent) val extraClassPath = builds.head.options.classPathOptions.extraClassPath.map { classPath => ClassPathEntry.Resource(classPath.last, os.mtime(classPath), os.read.bytes(classPath)) } val allEntries = Seq(byteCodeEntry) ++ dependencyEntries ++ extraClassPath val loaderContent = coursier.launcher.ClassLoaderContent(allEntries) val preamble = Preamble() .withOsKind(Properties.isWin) .callsItself(Properties.isWin) .withJavaOpts(builds.head.options.javaOptions.javaOpts.toSeq.map(_.value.value)) val baseParams = Parameters.Bootstrap(Seq(loaderContent), mainClass) .withDeterministic(true) .withPreamble(preamble) val params: Parameters.Bootstrap = if builds.head.options.notForBloopOptions.doSetupPython.getOrElse(false) then { val res = value { Artifacts.fetchAnyDependencies( Seq(Positioned.none( dep"${Constants.pythonInterfaceOrg}:${Constants.pythonInterfaceName}:${Constants.pythonInterfaceVersion}" )), Nil, None, logger, builds.head.options.finalCache, None, Some(_) ) } val entries = res.artifacts.map { case (a, f) => val path = os.Path(f) if builds.head.options.notForBloopOptions.packageOptions.isStandalone then ClassPathEntry.Resource(path.last, os.mtime(path), os.read.bytes(path)) else ClassPathEntry.Url(a.url) } val pythonContent = Seq(ClassLoaderContent(entries)) baseParams.addExtraContent("python", pythonContent).withPython(true) } else baseParams value(alreadyExistsCheck()) BootstrapGenerator.generate(params, destPath.toNIO) ProcUtil.maybeUpdatePreamble(destPath) } /** Returns the dependency sub-graph of the provided modules, that is, all their JARs and their * transitive dependencies' JARs. * * Note that this is not exactly the same as resolving those modules on their own (with their * versions): other dependencies in the whole dependency sub-graph may bump versions in the * provided dependencies sub-graph here. * * Here, among the JARs of the whole dependency graph, we pick the ones that were pulled by the * provided modules, and might have been bumped by other modules. This is strictly a subset of * the whole dependency graph. */ def providedFiles( builds: Seq[Build.Successful], provided: Seq[dependency.AnyModule], logger: Logger ): Either[BuildException, Seq[os.Path]] = either { logger.debug(s"${provided.length} provided dependencies") val res = builds.map(_.artifacts.resolution.getOrElse { sys.error("Internal error: expected resolution to have been kept") }) val modules: Seq[coursier.Module] = value { provided .map(_.toCs(builds.head.scalaParams)) // Scala params should be the same for all scopes .sequence .left.map(CompositeBuildException(_)) } val modulesSet = modules.toSet val providedDeps: Seq[core.Dependency] = value { res .map(_.dependencyArtifacts0.safeArtifacts.map(_.map(_._1))) .sequence .left .map(CompositeBuildException(_)) .map(_.flatten.filter(dep => modulesSet.contains(dep.module))) } val providedRes: Seq[Resolution] = value { res .map(_.subset0(providedDeps).left.map(CoursierDependencyError(_))) .sequence .left .map(CompositeBuildException(_)) } val fileMap = builds.flatMap(_.artifacts.detailedRuntimeArtifacts).distinct .map { case (_, _, artifact, path) => artifact -> path } .toMap val providedFiles: Seq[os.Path] = value { providedRes .map(r => coursier.Artifacts.artifacts0( resolution = r, classifiers = Set.empty, attributes = Seq.empty, mainArtifactsOpt = None, artifactTypesOpt = None, classpathOrder = true ).safeArtifacts ) .sequence .left .map(CompositeBuildException(_)) .map { _.flatten .distinct .map(_._3) .map(a => fileMap.getOrElse(a, sys.error(s"should not happen (missing: $a)"))) } } logger.debug { val it = Iterator(s"${providedFiles.size} provided JAR(s)") ++ providedFiles.toVector.map(_.toString).sorted.iterator.map(f => s" $f") it.mkString(System.lineSeparator()) } providedFiles } def assembly( builds: Seq[Build.Successful], destPath: os.Path, mainClassOpt: Option[String], extraProvided: Seq[dependency.AnyModule], withPreamble: Boolean, alreadyExistsCheck: () => Either[BuildException, Unit], logger: Logger ): Either[BuildException, Unit] = either { val compiledClassesByOutputDir: Seq[(Path, Path)] = builds.flatMap(build => os.walk(build.output).filter(os.isFile(_)).map(build.output -> _) ).distinct val (extraClassesFolders, extraJars) = builds.flatMap(_.options.classPathOptions.extraClassPath).partition(os.isDir(_)) val extraClassesByDefaultOutputDir = extraClassesFolders.flatMap(os.walk(_)).filter(os.isFile(_)).map(builds.head.output -> _) val byteCodeZipEntries = (compiledClassesByOutputDir ++ extraClassesByDefaultOutputDir) .distinct .map { (outputDir, path) => val name = path.relativeTo(outputDir).toString val content = os.read.bytes(path) val lastModified = os.mtime(path) val ent = new ZipEntry(name) ent.setLastModifiedTime(FileTime.fromMillis(lastModified)) ent.setSize(content.length) (ent, content) } val provided = builds.head.options.notForBloopOptions.packageOptions.provided ++ extraProvided val allJars = builds.flatMap(_.artifacts.runtimeArtifacts.map(_._2)) ++ extraJars.filter(os.exists(_)) val jars = if (provided.isEmpty) allJars else { val providedFilesSet = value(providedFiles(builds, provided, logger)).toSet allJars.filterNot(providedFilesSet.contains) } val preambleOpt = if withPreamble then Some { Preamble() .withOsKind(Properties.isWin) .callsItself(Properties.isWin) } else None val params = Parameters.Assembly() .withExtraZipEntries(byteCodeZipEntries) .withFiles(jars.map(_.toIO)) .withMainClass(mainClassOpt) .withPreambleOpt(preambleOpt) value(alreadyExistsCheck()) AssemblyGenerator.generate(params, destPath.toNIO) ProcUtil.maybeUpdatePreamble(destPath) } final class NoMainClassFoundForAssemblyError(cause: NoMainClassFoundError) extends BuildException( "No main class found for assembly. Either pass one with --main-class, or make the assembly non-runnable with --preamble=false", cause = cause ) private object LinkingDir { case class Input(linkJsInput: ScalaJsLinker.LinkJSInput, scratchDirOpt: Option[os.Path]) private var currentInput: Option[Input] = None private var currentLinkingDir: Option[os.Path] = None def getOrCreate( linkJsInput: ScalaJsLinker.LinkJSInput, scratchDirOpt: Option[os.Path] ): os.Path = val input = Input(linkJsInput, scratchDirOpt) currentLinkingDir match { case Some(linkingDir) if currentInput.contains(input) => linkingDir case _ => scratchDirOpt.foreach(os.makeDir.all(_)) currentLinkingDir.foreach(dir => os.remove.all(dir)) currentLinkingDir = None val linkingDirectory = os.temp.dir( dir = scratchDirOpt.orNull, prefix = "scala-cli-js-linking", deleteOnExit = scratchDirOpt.isEmpty ) currentInput = Some(input) currentLinkingDir = Some(linkingDirectory) linkingDirectory } } def linkJs( builds: Seq[Build.Successful], dest: os.Path, mainClassOpt: Option[String], addTestInitializer: Boolean, config: ScalaJsLinkerConfig, fullOpt: Boolean, noOpt: Boolean, logger: Logger, scratchDirOpt: Option[os.Path] = None ): Either[BuildException, os.Path] = { val jar = Library.libraryJar(builds) val classPath = Seq(jar) ++ builds.flatMap(_.artifacts.classPath) val input = ScalaJsLinker.LinkJSInput( options = builds.head.options.notForBloopOptions.scalaJsLinkerOptions, javaCommand = builds.head.options.javaHome().value.javaCommand, // FIXME Allow users to use another JVM here? classPath = classPath, mainClassOrNull = mainClassOpt.orNull, addTestInitializer = addTestInitializer, config = config, fullOpt = fullOpt, noOpt = noOpt, scalaJsVersion = builds.head.options.scalaJsOptions.finalVersion ) val linkingDir = LinkingDir.getOrCreate(input, scratchDirOpt) either { value { ScalaJsLinker.link( input, linkingDir, logger, builds.head.options.finalCache, builds.head.options.archiveCache ) } os.walk.stream(linkingDir).filter(_.ext == "js").toSeq match { case Seq(sourceJs) if os.isFile(sourceJs) && sourceJs.last.endsWith(".js") => // there's just one js file to link, so we copy it directly logger.debug( s"Scala.js linker generated single file ${sourceJs.last}. Copying it to $dest" ) val sourceMapJs = os.Path(sourceJs.toString + ".map") os.copy(sourceJs, dest, replaceExisting = true) if builds.head.options.scalaJsOptions.emitSourceMaps && os.exists(sourceMapJs) then { logger.debug( s"Source maps emission enabled, copying source map file: ${sourceMapJs.last}" ) val sourceMapDest = builds.head.options.scalaJsOptions.sourceMapsDest.getOrElse(os.Path(s"$dest.map")) val updatedMainJs = ScalaJsLinker.updateSourceMappingURL(dest) os.write.over(dest, updatedMainJs) os.copy(sourceMapJs, sourceMapDest, replaceExisting = true) logger.message(s"Emitted js source maps to: $sourceMapDest") } dest case _ @Seq(jsSource, _*) => os.copy( linkingDir, dest, createFolders = true, replaceExisting = true, mergeFolders = true ) logger.debug( s"Scala.js linker generated multiple files for js multi-modules. Copied files to $dest directory." ) val jsFileToReturn = os.rel / { mainClassOpt match { case Some(_) if os.exists(linkingDir / "main.js") => "main.js" case Some(mc) if os.exists(linkingDir / s"$mc.js") => s"$mc.js" case _ => jsSource.relativeTo(linkingDir) } } dest / jsFileToReturn case Nil => logger.debug("Scala.js linker did not generate any .js files.") val allFilesInLinkingDir = os.walk(linkingDir).map(_.relativeTo(linkingDir)) value(Left(new ScalaJsLinkingError( expected = if mainClassOpt.nonEmpty then "main.js" else ".js", foundFiles = allFilesInLinkingDir ))) } } } def buildNative( builds: Seq[Build.Successful], mainClass: Option[String], // when building a static/dynamic library, we don't need a main class targetType: PackageType.Native, destPath: Option[os.Path], logger: Logger ): Either[BuildException, os.Path] = either { val dest = builds.head.inputs.nativeWorkDir / s"main${if (Properties.isWin) ".exe" else ""}" val cliOptions = builds.head.options.scalaNativeOptions.configCliOptions(builds.exists( _.sources.resourceDirs.nonEmpty )) val setupPython = builds.head.options.notForBloopOptions.doSetupPython.getOrElse(false) val pythonLdFlags = if setupPython then value { val python = value(createPythonInstance().orPythonDetectionError) val flagsOrError = python.ldflags logger.debug(s"Python ldflags: $flagsOrError") flagsOrError.orPythonDetectionError } else Nil val pythonCliOptions = pythonLdFlags.flatMap(f => Seq("--linking-option", f)).toList val libraryLinkingOptions: Seq[String] = Option.when(targetType != PackageType.Native.Application) { /* If we are building a library, we make sure to change the name that the linker will put into the loading path - otherwise the built library will depend on some internal path within .scala-build */ destPath.flatMap(_.lastOpt).toSeq.flatMap { filename => val linkerOption = if Properties.isLinux then s"-Wl,-soname,$filename" else s"-Wl,-install_name,$filename" Seq("--linking-option", linkerOption) } }.toSeq.flatten val allCliOptions = pythonCliOptions ++ cliOptions ++ libraryLinkingOptions ++ mainClass.toSeq.flatMap(m => Seq("--main", m)) val nativeWorkDir = builds.head.inputs.nativeWorkDir os.makeDir.all(nativeWorkDir) val cacheData = CachedBinary.getCacheData( builds, allCliOptions, dest, nativeWorkDir ) if (cacheData.changed) { builds.foreach(build => NativeResourceMapper.copyCFilesToScalaNativeDir(build, nativeWorkDir)) val jar = Library.libraryJar(builds) val classpath = (Seq(jar) ++ builds.flatMap(_.artifacts.classPath)).map(_.toString).distinct val args = allCliOptions ++ logger.scalaNativeCliInternalLoggerOptions ++ List[String]( "--outpath", dest.toString(), "--workdir", nativeWorkDir.toString() ) ++ classpath val scalaNativeCli = builds.flatMap(_.artifacts.scalaOpt).headOption .getOrElse { sys.error("Expected Scala artifacts to be fetched") } .scalaNativeCli val exitCode = Runner.runJvm( builds.head.options.javaHome().value.javaCommand, builds.head.options.javaOptions.javaOpts.toSeq.map(_.value.value), scalaNativeCli, "scala.scalanative.cli.ScalaNativeLd", args, logger ).waitFor() if exitCode == 0 then CachedBinary.updateProjectAndOutputSha(dest, nativeWorkDir, cacheData.projectSha) else throw new ScalaNativeBuildError } dest } def resolvePackageType( builds: Seq[Build.Successful], forcedPackageTypeOpt: Option[PackageType] ): Either[BuildException, PackageType] = { val basePackageTypeOpt = builds.head.options.notForBloopOptions.packageOptions.packageTypeOpt lazy val validPackageScalaJS = Seq(PackageType.Js, PackageType.LibraryJar, PackageType.SourceJar, PackageType.DocJar) lazy val validPackageScalaNative = Seq( PackageType.LibraryJar, PackageType.SourceJar, PackageType.DocJar, PackageType.Native.Application, PackageType.Native.LibraryDynamic, PackageType.Native.LibraryStatic ) forcedPackageTypeOpt -> builds.head.options.platform.value match { case (Some(forcedPackageType), _) => Right(forcedPackageType) case (_, _) if builds.head.options.notForBloopOptions.packageOptions.isDockerEnabled => basePackageTypeOpt match { case Some(PackageType.Docker) | None => Right(PackageType.Docker) case Some(packageType) => Left(new MalformedCliInputError( s"Unsupported package type: $packageType for Docker." )) } case (_, Platform.JS) => { for (basePackageType <- basePackageTypeOpt) yield if validPackageScalaJS.contains(basePackageType) then Right(basePackageType) else Left(new MalformedCliInputError( s"Unsupported package type: $basePackageType for Scala.js." )) }.getOrElse(Right(PackageType.Js)) case (_, Platform.Native) => { val specificNativePackageType: Option[Native] = import ScalaNativeTarget.* builds.head.options.scalaNativeOptions.buildTargetStr.flatMap(fromString).map { case Application => PackageType.Native.Application case LibraryDynamic => PackageType.Native.LibraryDynamic case LibraryStatic => PackageType.Native.LibraryStatic } for basePackageType <- specificNativePackageType orElse basePackageTypeOpt yield if validPackageScalaNative.contains(basePackageType) then Right(basePackageType) else Left(new MalformedCliInputError( s"Unsupported package type: $basePackageType for Scala Native." )) }.getOrElse(Right(PackageType.Native.Application)) case _ => Right(basePackageTypeOpt.getOrElse(PackageType.Bootstrap)) } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/package0/PackageOptions.scala ================================================ package scala.cli.commands.package0 import caseapp.* import caseapp.core.help.Help import scala.build.EitherCps.{either, value} import scala.build.Ops.* import scala.build.compiler.{ScalaCompilerMaker, SimpleScalaCompilerMaker} import scala.build.errors.{BuildException, CompositeBuildException, ModuleFormatError} import scala.build.options.* import scala.build.options.packaging.* import scala.build.{BuildThreads, Logger, Positioned} import scala.cli.commands.shared.* import scala.cli.commands.tags @HelpMessage(PackageOptions.helpMessage, "", PackageOptions.detailedHelpMessage) // format: off final case class PackageOptions( @Recurse shared: SharedOptions = SharedOptions(), @Recurse watch: SharedWatchOptions = SharedWatchOptions(), @Recurse java: SharedJavaOptions = SharedJavaOptions(), @Recurse compileCross: CrossOptions = CrossOptions(), @Recurse mainClass: MainClassOptions = MainClassOptions(), @Group(HelpGroup.Package.toString) @HelpMessage("Set the destination path") @Name("o") @Tag(tags.restricted) @Tag(tags.inShortHelp) output: Option[String] = None, @Group(HelpGroup.Package.toString) @HelpMessage("Overwrite the destination file, if it exists") @Name("f") @Tag(tags.restricted) @Tag(tags.inShortHelp) force: Boolean = false, @Group(HelpGroup.Package.toString) @HelpMessage("Generate a library JAR rather than an executable JAR") @Tag(tags.restricted) @Tag(tags.inShortHelp) library: Boolean = false, @Group(HelpGroup.Package.toString) @HelpMessage("Generate a source JAR rather than an executable JAR") @Name("sourcesJar") @Name("jarSources") @Name("sources") @Tag(tags.deprecated("sources")) @Name("src") @Tag(tags.deprecated("src")) @Tag(tags.restricted) @Tag(tags.inShortHelp) withSources: Boolean = false, @Group(HelpGroup.Package.toString) @HelpMessage("Generate a scaladoc JAR rather than an executable JAR") @ExtraName("scaladoc") @ExtraName("javadoc") @Tag(tags.restricted) @Tag(tags.inShortHelp) doc: Boolean = false, @Group(HelpGroup.Package.toString) @HelpMessage("Generate an assembly JAR") @Tag(tags.restricted) @Tag(tags.inShortHelp) assembly: Boolean = false, @Group(HelpGroup.Package.toString) @HelpMessage("For assembly JAR, whether to add a bash / bat preamble") @Tag(tags.restricted) @Tag(tags.inShortHelp) preamble: Boolean = true, @Group(HelpGroup.Package.toString) @Hidden @HelpMessage("For assembly JAR, whether to specify a main class in the JAR manifest") @Tag(tags.restricted) mainClassInManifest: Option[Boolean] = None, @Group(HelpGroup.Package.toString) @Hidden @HelpMessage("Generate an assembly JAR for Spark (assembly that doesn't contain Spark, nor any of its dependencies)") @Tag(tags.experimental) spark: Boolean = false, @Group(HelpGroup.Package.toString) @HelpMessage("Package standalone JARs") @Tag(tags.restricted) @Tag(tags.inShortHelp) standalone: Option[Boolean] = None, @Recurse packager: PackagerOptions = PackagerOptions(), @Group(HelpGroup.Package.toString) @HelpMessage("Build Debian package, available only on Linux") @Tag(tags.restricted) @Tag(tags.inShortHelp) deb: Boolean = false, @Group(HelpGroup.Package.toString) @HelpMessage("Build dmg package, available only on macOS") @Tag(tags.restricted) @Tag(tags.inShortHelp) dmg: Boolean = false, @Group(HelpGroup.Package.toString) @HelpMessage("Build rpm package, available only on Linux") @Tag(tags.restricted) @Tag(tags.inShortHelp) rpm: Boolean = false, @Group(HelpGroup.Package.toString) @HelpMessage("Build msi package, available only on Windows") @Tag(tags.restricted) @Tag(tags.inShortHelp) msi: Boolean = false, @Group(HelpGroup.Package.toString) @HelpMessage("Build pkg package, available only on macOS") @Tag(tags.restricted) @Tag(tags.inShortHelp) pkg: Boolean = false, @Group(HelpGroup.Package.toString) @HelpMessage("Build Docker image") @Tag(tags.restricted) @Tag(tags.inShortHelp) docker: Boolean = false, @Group(HelpGroup.Package.toString) @Hidden @HelpMessage("Exclude modules *and their transitive dependencies* from the JAR to be packaged") @ValueDescription("org:name") @Tag(tags.restricted) @Tag(tags.inShortHelp) provided: List[String] = Nil, @Group(HelpGroup.Package.toString) @HelpMessage("Use default scaladoc options") @ExtraName("defaultScaladocOpts") @Tag(tags.implementation) defaultScaladocOptions: Option[Boolean] = None, @Group(HelpGroup.Package.toString) @HelpMessage("Build GraalVM native image") @ExtraName("graal") @Tag(tags.restricted) @Tag(tags.inShortHelp) nativeImage: Boolean = false ) extends HasSharedOptions with HasSharedWatchOptions { // format: on def packageTypeOpt: Option[PackageType] = forcedPackageTypeOpt.orElse { if (library) Some(PackageType.LibraryJar) else if (withSources) Some(PackageType.SourceJar) else if (assembly) Some( PackageType.Assembly( addPreamble = preamble, mainClassInManifest = mainClassInManifest ) ) else if (spark) Some(PackageType.Spark) else if (deb) Some(PackageType.Debian) else if (dmg) Some(PackageType.Dmg) else if (pkg) Some(PackageType.Pkg) else if (rpm) Some(PackageType.Rpm) else if (msi) Some(PackageType.Msi) else if (nativeImage) Some(PackageType.GraalVMNativeImage) else None } def forcedPackageTypeOpt: Option[PackageType] = if (doc) Some(PackageType.DocJar) else None def providedModules: Either[BuildException, Seq[dependency.AnyModule]] = provided .map { str => dependency.parser.ModuleParser.parse(str) .left.map(err => new ModuleFormatError(str, err)) } .sequence .left.map(CompositeBuildException(_)) def baseBuildOptions(logger: Logger): Either[BuildException, BuildOptions] = either { val baseOptions = value(buildOptions()) baseOptions.copy( mainClass = mainClass.mainClass.filter(_.nonEmpty), notForBloopOptions = baseOptions.notForBloopOptions.copy( packageOptions = baseOptions.notForBloopOptions.packageOptions.copy( standalone = standalone, version = Some(packager.version), launcherApp = packager.launcherApp, maintainer = packager.maintainer, description = packager.description, output = output, packageTypeOpt = packageTypeOpt, logoPath = packager.logoPath.map(os.Path(_, os.pwd)), macOSidentifier = packager.identifier, debianOptions = DebianOptions( conflicts = packager.debianConflicts, dependencies = packager.debianDependencies, architecture = Some(packager.debArchitecture), priority = packager.priority, section = packager.section ), redHatOptions = RedHatOptions( license = packager.license, release = Some(packager.release), architecture = Some(packager.rpmArchitecture) ), windowsOptions = WindowsOptions( licensePath = packager.licensePath.map(os.Path(_, os.pwd)), productName = Some(packager.productName), exitDialog = packager.exitDialog, suppressValidation = packager.suppressValidation, extraConfig = packager.extraConfig, is64Bits = Some(packager.is64Bits), installerVersion = packager.installerVersion, wixUpgradeCodeGuid = packager.wixUpgradeCodeGuid ), dockerOptions = DockerOptions( from = packager.dockerFrom, imageRegistry = packager.dockerImageRegistry, imageRepository = packager.dockerImageRepository, imageTag = packager.dockerImageTag, cmd = packager.dockerCmd, isDockerEnabled = Some(docker), extraDirectories = packager.dockerExtraDirectories.map(os.Path(_, os.pwd)) ), nativeImageOptions = { val graalVmVersion = packager.graalvmVersion.map(_.trim).filter(_.nonEmpty) val graalVmJavaVersion = packager.graalvmJvmId.map(_.trim) for { vmVersion <- graalVmVersion javaVersion <- graalVmJavaVersion if !vmVersion.startsWith(javaVersion) } logger.message( s"""GraalVM Java major version ($javaVersion) does not match GraalVM version ($vmVersion). |GraalVM version should start with the Java major version to be used.""".stripMargin ) NativeImageOptions( graalvmJvmId = packager.graalvmJvmId.map(_.trim).filter(_.nonEmpty), graalvmJavaVersion = packager.graalvmJavaVersion.filter(_ > 0), graalvmVersion = graalVmVersion, graalvmArgs = packager.graalvmArgs.map(_.trim).filter(_.nonEmpty).map(Positioned.commandLine) ) }, useDefaultScaladocOptions = defaultScaladocOptions ), addRunnerDependencyOpt = Some(false) ), internal = baseOptions.internal.copy( // computing the provided modules sub-graph need the final Resolution instance // Spark packaging adds provided modules, so it needs it too keepResolution = provided.nonEmpty || packageTypeOpt.contains(PackageType.Spark) ) ) } def finalBuildOptions(logger: Logger): Either[BuildException, BuildOptions] = either { val baseOptions = value(baseBuildOptions(logger)) baseOptions.copy( notForBloopOptions = baseOptions.notForBloopOptions.copy( packageOptions = baseOptions.notForBloopOptions.packageOptions.copy( provided = value(providedModules) ) ) ) } def compilerMaker(threads: BuildThreads): Either[BuildException, ScalaCompilerMaker] = either { val maker = shared.compilerMaker(threads) if (forcedPackageTypeOpt.contains(PackageType.DocJar)) ScalaCompilerMaker.IgnoreScala2(maker) else maker } def docCompilerMakerOpt: Option[ScalaCompilerMaker] = if (forcedPackageTypeOpt.contains(PackageType.DocJar)) Some(SimpleScalaCompilerMaker("java", Nil, scaladoc = true)) else None } object PackageOptions { implicit lazy val parser: Parser[PackageOptions] = Parser.derive implicit lazy val help: Help[PackageOptions] = Help.derive val cmdName = "package" private val helpHeader = "Compile and package Scala code." val helpMessage: String = HelpMessages.shortHelpMessage(cmdName, helpHeader, needsPower = true) val detailedHelpMessage: String = s"""$helpHeader | |${HelpMessages.commandConfigurations(cmdName)} | |${HelpMessages.acceptedInputs} | |${HelpMessages.commandDocWebsiteReference(cmdName)}""".stripMargin } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/package0/PackagerOptions.scala ================================================ package scala.cli.commands.package0 import caseapp.* import scala.cli.commands.shared.HelpGroup import scala.cli.commands.{Constants, tags} // format: off final case class PackagerOptions( @HelpMessage("Set the version of the generated package") @Tag(tags.restricted) version: String = "1.0.0", @HelpMessage( "Path to application logo in PNG format, it will be used to generate icon and banner/dialog in msi installer" ) @Tag(tags.restricted) logoPath: Option[String] = None, @HelpMessage("Set launcher app name, which will be linked to the PATH") @Tag(tags.restricted) launcherApp: Option[String] = None, @ValueDescription("Description") description: Option[String] = None, @HelpMessage("This should contain names and email addresses of co-maintainers of the package") @Name("m") maintainer: Option[String] = None, @Group(HelpGroup.Debian.toString) @HelpMessage( "The list of Debian package that this package is not compatible with" ) @ValueDescription("Debian dependencies conflicts") @Tag(tags.restricted) debianConflicts: List[String] = Nil, @Group(HelpGroup.Debian.toString) @HelpMessage("The list of Debian packages that this package depends on") @ValueDescription("Debian dependencies") @Tag(tags.restricted) debianDependencies: List[String] = Nil, @Group(HelpGroup.Debian.toString) @HelpMessage( "Architectures that are supported by the repository (default: all)" ) @Tag(tags.restricted) debArchitecture: String = "all", @Group(HelpGroup.Debian.toString) @HelpMessage( "This field represents how important it is that the user have the package installed" ) @Tag(tags.restricted) priority: Option[String] = None, @Group(HelpGroup.Debian.toString) @HelpMessage( "This field specifies an application area into which the package has been classified" ) @Tag(tags.restricted) section: Option[String] = None, @Group(HelpGroup.MacOS.toString) @HelpMessage( "CF Bundle Identifier" ) @Tag(tags.restricted) identifier: Option[String] = None, @Group(HelpGroup.RedHat.toString) @HelpMessage( "Licenses that are supported by the repository (list of licenses: https://spdx.org/licenses/)" ) @Tag(tags.restricted) license: Option[String] = None, @Group(HelpGroup.RedHat.toString) @HelpMessage( "The number of times this version of the software was released (default: 1)" ) @Tag(tags.restricted) release: String = "1", @HelpMessage("Architectures that are supported by the repository (default: noarch)") @Tag(tags.restricted) rpmArchitecture: String = "noarch", @Group(HelpGroup.Windows.toString) @HelpMessage("Path to the license file") @Tag(tags.restricted) licensePath: Option[String] = None, @Group(HelpGroup.Windows.toString) @HelpMessage("Name of product (default: Scala packager)") @Tag(tags.restricted) productName: String = "Scala packager", @Group(HelpGroup.Windows.toString) @HelpMessage("Text that will be displayed on the exit dialog") @Tag(tags.restricted) exitDialog: Option[String] = None, @Group(HelpGroup.Windows.toString) @Tag(tags.restricted) @HelpMessage("Suppress Wix ICE validation (required for users that are neither interactive, not local administrators)") suppressValidation: Option[Boolean] = None, @Group(HelpGroup.Windows.toString) @Tag(tags.restricted) @HelpMessage("Path to extra WIX configuration content") @ValueDescription("path") extraConfig: List[String] = Nil, @Group(HelpGroup.Windows.toString) @Tag(tags.restricted) @HelpMessage("Whether a 64-bit executable is being packaged") @Name("64") is64Bits: Boolean = true, @Group(HelpGroup.Windows.toString) @HelpMessage("WIX installer version") @Tag(tags.restricted) installerVersion: Option[String] = None, @Group(HelpGroup.Windows.toString) @HelpMessage("The GUID to identify that the windows package can be upgraded.") @Tag(tags.restricted) wixUpgradeCodeGuid: Option[String] = None, @Group(HelpGroup.Docker.toString) @HelpMessage( "Building the container from base image" ) @Tag(tags.restricted) dockerFrom: Option[String] = None, @Group(HelpGroup.Docker.toString) @HelpMessage( "The image registry; if empty, it will use the default registry" ) @Tag(tags.restricted) dockerImageRegistry: Option[String] = None, @Group(HelpGroup.Docker.toString) @HelpMessage( "The image repository" ) @Tag(tags.restricted) dockerImageRepository: Option[String] = None, @Group(HelpGroup.Docker.toString) @HelpMessage( "The image tag; the default tag is `latest`" ) @Tag(tags.restricted) dockerImageTag: Option[String] = None, @Group(HelpGroup.Docker.toString) @HelpMessage( "Allows to override the executable used to run the application in docker, otherwise it defaults to sh for the JVM platform and node for the JS platform" ) @Tag(tags.restricted) dockerCmd: Option[String] = None, @Group(HelpGroup.Docker.toString) @HelpMessage("Extra directories to be added to the docker image") @Tag(tags.restricted) dockerExtraDirectories: List[String] = Nil, @Group(HelpGroup.NativeImage.toString) @HelpMessage(s"GraalVM Java major version to use to build GraalVM native images (${Constants.defaultGraalVMJavaVersion} by default)") @ValueDescription("java-major-version") @Tag(tags.restricted) @Tag(tags.inShortHelp) graalvmJavaVersion: Option[Int] = None, @Group(HelpGroup.NativeImage.toString) @HelpMessage(s"GraalVM version to use to build GraalVM native images (${Constants.defaultGraalVMVersion} by default)") @ValueDescription("version") @Tag(tags.inShortHelp) graalvmVersion: Option[String] = None, @Group(HelpGroup.NativeImage.toString) @HelpMessage("JVM id of GraalVM distribution to build GraalVM native images (like \"graalvm-java17:22.0.0\")") @ValueDescription("jvm-id") @Tag(tags.restricted) graalvmJvmId: Option[String] = None, @Group(HelpGroup.NativeImage.toString) @HelpMessage("Pass args to GraalVM") @Tag(tags.restricted) graalvmArgs: List[String] = Nil ) // format: on object PackagerOptions { implicit lazy val parser: Parser[PackagerOptions] = Parser.derive implicit lazy val help: Help[PackagerOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/packaging/Spark.scala ================================================ package scala.cli.commands.packaging import dependency.* object Spark { private def names = Seq( // FIXME Add more? // (see "cs complete-dependency org.apache.spark: | grep '_2\.12$'" // or `ls "$(cs get https://archive.apache.org/dist/spark/spark-2.4.2/spark-2.4.2-bin-hadoop2.7.tgz --archive)"/*/jars | grep '^spark-'`) "core", "graphx", "hive", "hive-thriftserver", "kubernetes", "mesos", "mllib", "repl", "sql", "streaming", "yarn" ) def sparkModules: Seq[AnyModule] = names.map(name => mod"org.apache.spark::spark-$name") def hadoopModules: Seq[AnyModule] = Seq( // TODO Add more for Hadoop 2, maybe for 3 too mod"org.apache.hadoop:hadoop-client-api" ) } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/pgp/DummyOptions.scala ================================================ package scala.cli.commands.pgp import caseapp.* final case class DummyOptions() object DummyOptions { implicit lazy val parser: Parser[DummyOptions] = Parser.derive implicit lazy val help: Help[DummyOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/pgp/ExternalCommand.scala ================================================ package scala.cli.commands.pgp import caseapp.* import scala.cli.commands.util.CommandHelpers abstract class ExternalCommand extends Command[DummyOptions] with CommandHelpers { override def hasHelp = false override def stopAtFirstUnrecognized = true def actualHelp: Help[?] def run(options: DummyOptions, args: RemainingArgs): Unit = { val unparsedPart = if (args.unparsed.isEmpty) Nil else Seq("--") ++ args.unparsed val allArgs = args.remaining ++ unparsedPart run(allArgs) } def run(args: Seq[String]): Unit } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/pgp/KeyServer.scala ================================================ package scala.cli.commands.pgp import sttp.client3.* import sttp.model.Uri object KeyServer { def default = // wouldn't https://keyserver.ubuntu.com work as well (https > http) uri"http://keyserver.ubuntu.com:11371" def allDefaults = Seq( default // Sonatype sometimes mentions this one when the key wasn't uploaded anywhere, // but lookups are too slow there (and often timeout actually) // seems https://pgp.mit.edu might work too // uri"http://pgp.mit.edu:11371" ) def add( pubKey: String, keyServer: Uri, backend: SttpBackend[Identity, Any] ): Either[String, String] = { val resp = basicRequest .body(Map("keytext" -> pubKey)) .response(asString) .post(keyServer.addPath("pks", "add")) .send(backend) if (resp.isSuccess) Right(resp.body.merge) else Left(resp.body.merge) } def check( keyId: String, keyServer: Uri, backend: SttpBackend[Identity, Any] ): Either[String, Either[String, String]] = { val resp = basicRequest .get(keyServer.addPath("pks", "lookup").addParam("op", "get").addParam("search", keyId)) .response(asString) .send(backend) if (resp.isSuccess) Right(Right(resp.body.merge)) else if (resp.isClientError) Right(Left(resp.body.merge)) else Left(resp.body.merge) } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/pgp/PgpCommand.scala ================================================ package scala.cli.commands.pgp import caseapp.core.app.Command import caseapp.core.help.Help import caseapp.core.parser.Parser import scala.build.Logger import scala.build.input.{ScalaCliInvokeData, SubCommand} import scala.cli.ScalaCli import scala.cli.commands.RestrictableCommand import scala.cli.commands.util.CommandHelpers import scala.cli.internal.{CliLogger, ProcUtil} abstract class PgpCommand[T](implicit myParser: Parser[T], help: Help[T]) extends Command()(using myParser, help) with CommandHelpers with RestrictableCommand[T] { override protected def invokeData: ScalaCliInvokeData = ScalaCliInvokeData( progName = ScalaCli.progName, subCommandName = name, // FIXME Should be the actual name that was called from the command line subCommand = SubCommand.Other, isShebangCapableShell = ProcUtil.isShebangCapableShell ) override def scalaSpecificationLevel = SpecificationLevel.EXPERIMENTAL override def shouldSuppressExperimentalFeatureWarnings: Boolean = false // TODO add handling for scala-cli-signing override def shouldSuppressDeprecatedFeatureWarnings: Boolean = false // TODO add handling for scala-cli-signing override def logger: Logger = CliLogger.default // TODO add handling for scala-cli-signing override def hidden = true } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/pgp/PgpCommandNames.scala ================================================ package scala.cli.commands.pgp object PgpCommandNames { def pgpCreate = List( List("pgp", "create") ) def pgpKeyId = List( List("pgp", "key-id") ) def pgpSign = List( List("pgp", "sign") ) def pgpVerify = List( List("pgp", "verify") ) } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/pgp/PgpCommands.scala ================================================ package scala.cli.commands.pgp class PgpCommands { def allScalaCommands: Array[PgpCommand[?]] = Array(PgpCreate, PgpKeyId, PgpSign, PgpVerify) def allExternalCommands: Array[ExternalCommand] = Array.empty } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/pgp/PgpCreate.scala ================================================ package scala.cli.commands.pgp import caseapp.core.RemainingArgs import scala.cli.signing.commands.{PgpCreate as OriginalPgpCreate, PgpCreateOptions} object PgpCreate extends PgpCommand[PgpCreateOptions] { override def names = PgpCommandNames.pgpCreate override def run(options: PgpCreateOptions, args: RemainingArgs): Unit = OriginalPgpCreate.run(options, args) } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/pgp/PgpCreateExternal.scala ================================================ package scala.cli.commands.pgp import scala.cli.signing.commands.PgpCreateOptions class PgpCreateExternal extends PgpExternalCommand { override def hidden = true def actualHelp = PgpCreateOptions.help def externalCommand = Seq("pgp", "create") override def names = PgpCommandNames.pgpCreate } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/pgp/PgpExternalCommand.scala ================================================ package scala.cli.commands.pgp import coursier.cache.{ArchiveCache, FileCache} import coursier.util.Task import dependency.* import java.io.File import scala.build.EitherCps.{either, value} import scala.build.Ops.* import scala.build.errors.BuildException import scala.build.internal.Util.DependencyOps import scala.build.internal.{ Constants, ExternalBinary, ExternalBinaryParams, FetchExternalBinary, Runner } import scala.build.{Logger, Positioned, RepositoryUtils, options as bo} import scala.cli.ScalaCli import scala.cli.commands.shared.{CoursierOptions, SharedJvmOptions} import scala.cli.commands.util.JvmUtils import scala.util.Properties abstract class PgpExternalCommand extends ExternalCommand { def progName: String = ScalaCli.progName def externalCommand: Seq[String] def tryRun( cache: FileCache[Task], args: Seq[String], extraEnv: Map[String, String], logger: Logger, allowExecve: Boolean, jvmOptions: SharedJvmOptions, coursierOptions: CoursierOptions, signingCliOptions: bo.ScalaSigningCliOptions ): Either[BuildException, Int] = either { val archiveCache = ArchiveCache().withCache(cache) val binary = value(PgpExternalCommand.launcher( cache, archiveCache, logger, jvmOptions, coursierOptions, signingCliOptions )) val command = binary ++ externalCommand ++ args Runner.run0( progName, command, logger, allowExecve = allowExecve, cwd = None, extraEnv = extraEnv, inheritStreams = true ).waitFor() } def output( cache: FileCache[Task], args: Seq[String], extraEnv: Map[String, String], logger: Logger, jvmOptions: SharedJvmOptions, coursierOptions: CoursierOptions, signingCliOptions: bo.ScalaSigningCliOptions ): Either[BuildException, String] = either { val archiveCache = ArchiveCache().withCache(cache) val binary = value(PgpExternalCommand.launcher( cache, archiveCache, logger, jvmOptions, coursierOptions, signingCliOptions )) val command = binary ++ externalCommand ++ args os.proc(command).call(stdin = os.Inherit, env = extraEnv) .out.text() } def run(args: Seq[String]): Unit = { val (options, remainingArgs) = PgpExternalOptions.parser.stopAtFirstUnrecognized.parse(args) match { case Left(err) => System.err.println(err.message) sys.exit(1) case Right((options0, remainingArgs0)) => (options0, remainingArgs0) } val logger = options.global.logging.logger val cache = options.coursier.coursierCache(logger) val retCode = tryRun( cache, remainingArgs, Map(), logger, allowExecve = true, options.jvm, options.coursier, options.scalaSigning.cliOptions() ).orExit(logger) if (retCode != 0) sys.exit(retCode) } } object PgpExternalCommand { val scalaCliSigningJvmVersion: Int = Constants.signingCliJvmVersion def launcher( cache: FileCache[Task], archiveCache: ArchiveCache[Task], logger: Logger, jvmOptions: SharedJvmOptions, coursierOptions: CoursierOptions, signingCliOptions: bo.ScalaSigningCliOptions ): Either[BuildException, Seq[String]] = { val javaCommand = () => JvmUtils.getJavaCmdVersionOrHigher( scalaCliSigningJvmVersion, jvmOptions, coursierOptions ).orThrow.javaCommand launcher( cache, archiveCache, logger, javaCommand, signingCliOptions ) } def launcher( cache: FileCache[Task], archiveCache: ArchiveCache[Task], logger: Logger, buildOptions: bo.BuildOptions ): Either[BuildException, Seq[String]] = { val javaCommand = () => JvmUtils.getJavaCmdVersionOrHigher( scalaCliSigningJvmVersion, buildOptions ).orThrow.javaCommand launcher( cache, archiveCache, logger, javaCommand, buildOptions.notForBloopOptions.publishOptions.signingCli ) } private def launcher( cache: FileCache[Task], archiveCache: ArchiveCache[Task], logger: Logger, javaCommand: () => String, signingCliOptions: bo.ScalaSigningCliOptions ): Either[BuildException, Seq[String]] = either { val version = signingCliOptions.signingCliVersion .getOrElse(Constants.scalaCliSigningVersion) val ver = if (version.startsWith("latest")) "latest.release" else version val signingMainClass = "scala.cli.signing.ScalaCliSigning" val jvmSigningDep = dep"${Constants.scalaCliSigningOrganization}:${Constants.scalaCliSigningName}_3:$ver" if (signingCliOptions.forceJvm.getOrElse(false)) { val extraRepos = if version.endsWith("SNAPSHOT") then Seq( RepositoryUtils.snapshotsRepository, RepositoryUtils.scala3NightlyRepository ) else Nil val (_, signingRes) = value { scala.build.Artifacts.fetchCsDependencies( dependencies = Seq(Positioned.none(jvmSigningDep.toCs)), extraRepositories = extraRepos, forceScalaVersionOpt = None, forcedVersions = Nil, logger = logger, cache = cache, classifiersOpt = None ) } val signingClassPath = signingRes.files val command = Seq[os.Shellable]( javaCommand(), signingCliOptions.javaArgs, "-cp", signingClassPath.map(_.getAbsolutePath).mkString(File.pathSeparator), signingMainClass ) command.flatMap(_.value) } else { val platformSuffix = FetchExternalBinary.platformSuffix() val (tag, changing) = if (version == "latest") ("launchers", true) else ("v" + version, false) val ext = if (Properties.isWin) ".zip" else ".gz" val url = s"https://github.com/VirtusLab/scala-cli-signing/releases/download/$tag/scala-cli-signing-$platformSuffix$ext" val params = ExternalBinaryParams( url, changing, "scala-cli-signing", Seq(jvmSigningDep), signingMainClass ) val binary: ExternalBinary = value { FetchExternalBinary.fetch(params, archiveCache, logger, javaCommand) } binary.command } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/pgp/PgpExternalOptions.scala ================================================ package scala.cli.commands.pgp import caseapp.* import scala.cli.commands.shared.{ CoursierOptions, GlobalOptions, HasGlobalOptions, SharedJvmOptions } // format: off final case class PgpExternalOptions( @Recurse global: GlobalOptions = GlobalOptions(), @Recurse jvm: SharedJvmOptions = SharedJvmOptions(), @Recurse coursier: CoursierOptions = CoursierOptions(), @Recurse scalaSigning: PgpScalaSigningOptions = PgpScalaSigningOptions() ) extends HasGlobalOptions // format: on object PgpExternalOptions { implicit lazy val parser: Parser[PgpExternalOptions] = Parser.derive implicit lazy val help: Help[PgpExternalOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/pgp/PgpKeyId.scala ================================================ package scala.cli.commands.pgp import caseapp.core.RemainingArgs import scala.cli.signing.commands.{PgpKeyId as OriginalPgpKeyId, PgpKeyIdOptions} object PgpKeyId extends PgpCommand[PgpKeyIdOptions] { override def names: List[List[String]] = PgpCommandNames.pgpKeyId override def run(options: PgpKeyIdOptions, args: RemainingArgs): Unit = OriginalPgpKeyId.run(options, args) } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/pgp/PgpKeyIdExternal.scala ================================================ package scala.cli.commands.pgp import scala.cli.signing.commands.PgpKeyIdOptions class PgpKeyIdExternal extends PgpExternalCommand { override def hidden = true def actualHelp = PgpKeyIdOptions.help def externalCommand = Seq("pgp", "key-id") override def names = PgpCommandNames.pgpKeyId } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/pgp/PgpProxy.scala ================================================ package scala.cli.commands.pgp import coursier.cache.FileCache import coursier.util.Task import scala.build.errors.BuildException import scala.build.{Logger, options as bo} import scala.cli.commands.shared.{CoursierOptions, SharedJvmOptions} import scala.cli.errors.PgpError import scala.util.Properties /** A proxy running the PGP operations externally using scala-cli-singing. This is done either using * it's native image launchers or running it in a JVM process. This construct is not used when PGP * commands are evoked from CLI (see [[PgpCommandsSubst]] and [[PgpCommands]]), but rather when PGP * operations are used internally.
* * This is the 'native' counterpart of [[PgpProxyJvm]] */ class PgpProxy { def createKey( pubKey: String, secKey: String, mail: String, quiet: Boolean, passwordOpt: Option[String], cache: FileCache[Task], logger: Logger, jvmOptions: SharedJvmOptions, coursierOptions: CoursierOptions, signingCliOptions: bo.ScalaSigningCliOptions ): Either[BuildException, Int] = { val (passwordOption, extraEnv) = passwordOpt match case Some(value) => ( Seq("--password", s"env:SCALA_CLI_RANDOM_KEY_PASSWORD"), Map("SCALA_CLI_RANDOM_KEY_PASSWORD" -> value) ) case None => (Nil, Map.empty) val quietOptions = if quiet then Seq("--quiet") else Nil (new PgpCreateExternal).tryRun( cache, Seq( "pgp", "create", "--pub-dest", pubKey, "--secret-dest", secKey, "--email", mail ) ++ passwordOption ++ quietOptions, extraEnv, logger, allowExecve = false, jvmOptions, coursierOptions, signingCliOptions ) } def keyId( key: String, keyPrintablePath: String, cache: FileCache[Task], logger: Logger, jvmOptions: SharedJvmOptions, coursierOptions: CoursierOptions, signingCliOptions: bo.ScalaSigningCliOptions ): Either[BuildException, String] = { val keyPath = if (Properties.isWin) os.temp(key, prefix = "key", suffix = ".pub") else os.temp(key, prefix = "key", suffix = ".pub", perms = "rwx------") val maybeRawOutput = try { (new PgpKeyIdExternal).output( cache, Seq(keyPath.toString), Map(), logger, jvmOptions, coursierOptions, signingCliOptions ).map(_.trim) } finally os.remove(keyPath) maybeRawOutput.flatMap { rawOutput => if (rawOutput.isEmpty) Left(new PgpError(s"No public key found in $keyPrintablePath")) else Right(rawOutput) } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/pgp/PgpProxyJvm.scala ================================================ package scala.cli.commands.pgp import caseapp.core.RemainingArgs import coursier.cache.FileCache import coursier.util.Task import java.nio.charset.StandardCharsets import scala.build.errors.BuildException import scala.build.{Logger, options as bo} import scala.cli.commands.shared.{CoursierOptions, SharedJvmOptions} import scala.cli.errors.PgpError import scala.cli.signing.commands.{PgpCreate, PgpCreateOptions, PgpKeyId} import scala.cli.signing.shared.{PasswordOption, Secret} /** A proxy running the PGP operations using scala-cli-singing as a dependency. This construct is * not used when PGP commands are evoked from CLI (see [[PgpCommandsSubst]] and [[PgpCommands]]), * but rather when PGP operations are used internally.
* * This is the 'JVM' counterpart of [[PgpProxy]] */ class PgpProxyJvm extends PgpProxy { override def createKey( pubKey: String, secKey: String, mail: String, quiet: Boolean, passwordOpt: Option[String], cache: FileCache[Task], logger: Logger, jvmOptions: SharedJvmOptions, coursierOptions: CoursierOptions, signingCliOptions: bo.ScalaSigningCliOptions ): Either[BuildException, Int] = { val password = passwordOpt.map(password => PasswordOption.Value(Secret(password))) PgpCreate.tryRun( PgpCreateOptions( email = mail, password = password, pubDest = Some(pubKey), secretDest = Some(secKey), quiet = quiet ), RemainingArgs(Seq(), Nil) ) Right(0) } override def keyId( key: String, keyPrintablePath: String, cache: FileCache[Task], logger: Logger, jvmOptions: SharedJvmOptions, coursierOptions: CoursierOptions, signingCliOptions: bo.ScalaSigningCliOptions ): Either[BuildException, String] = PgpKeyId.get(key.getBytes(StandardCharsets.UTF_8), fingerprint = false) .headOption .toRight { new PgpError(s"No public key found in $keyPrintablePath") } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/pgp/PgpProxyMaker.scala ================================================ package scala.cli.commands.pgp /** Used for choosing the right PGP proxy implementation when Scala CLI is run on JVM.
* * See [[scala.cli.internal.PgpProxyMakerSubst PgpProxyMakerSubst]] */ class PgpProxyMaker { def get(forceSigningExternally: java.lang.Boolean): PgpProxy = if (forceSigningExternally) new PgpProxy else new PgpProxyJvm } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/pgp/PgpPull.scala ================================================ package scala.cli.commands.pgp import caseapp.core.RemainingArgs import scala.build.Logger import scala.cli.commands.ScalaCommand import scala.cli.commands.util.ScalaCliSttpBackend object PgpPull extends ScalaCommand[PgpPullOptions] { override def hidden = true override def scalaSpecificationLevel = SpecificationLevel.EXPERIMENTAL override def names = List( List("pgp", "pull") ) override def runCommand(options: PgpPullOptions, args: RemainingArgs, logger: Logger): Unit = { val backend = ScalaCliSttpBackend.httpURLConnection(logger) val keyServerUri = options.shared.keyServerUriOptOrExit(logger).getOrElse { KeyServer.default } val all = args.all if (!options.allowEmpty && all.isEmpty) { System.err.println("No key passed as argument.") sys.exit(1) } // val lookupEndpoint = keyServerUri for (keyId <- all) KeyServer.check(keyId, keyServerUri, backend) match { case Left(err) => System.err.println(s"Error checking $keyId: $err") sys.exit(1) case Right(Right(content)) => println(content) case Right(Left(message)) => if (logger.verbosity >= 0) System.err.println(s"Key $keyId not found: $message") sys.exit(1) } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/pgp/PgpPullOptions.scala ================================================ package scala.cli.commands.pgp import caseapp.* import scala.cli.commands.shared.{GlobalOptions, HasGlobalOptions, HelpGroup} // format: off final case class PgpPullOptions( @Recurse global: GlobalOptions = GlobalOptions(), @Recurse shared: SharedPgpPushPullOptions = SharedPgpPushPullOptions(), @Group(HelpGroup.PGP.toString) @HelpMessage("Whether to exit with code 0 if no key is passed") allowEmpty: Boolean = false ) extends HasGlobalOptions // format: on object PgpPullOptions { implicit lazy val parser: Parser[PgpPullOptions] = Parser.derive implicit lazy val help: Help[PgpPullOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/pgp/PgpPush.scala ================================================ package scala.cli.commands.pgp import caseapp.core.RemainingArgs import scala.build.Logger import scala.cli.commands.ScalaCommand import scala.cli.commands.util.ScalaCliSttpBackend object PgpPush extends ScalaCommand[PgpPushOptions] { override def hidden = true override def scalaSpecificationLevel = SpecificationLevel.EXPERIMENTAL override def names = List( List("pgp", "push") ) override def runCommand(options: PgpPushOptions, args: RemainingArgs, logger: Logger): Unit = { val backend = ScalaCliSttpBackend.httpURLConnection(logger) val keyServerUri = options.shared.keyServerUriOptOrExit(logger).getOrElse { KeyServer.default } val all = args.all if (!options.allowEmpty && all.isEmpty) { System.err.println("No key passed as argument.") sys.exit(1) } lazy val coursierCache = options.coursier.coursierCache(logger) for (key <- all) { val path = os.Path(key, os.pwd) if (!os.exists(path)) { System.err.println(s"Error: $key not found") sys.exit(1) } val keyContent = os.read(path) val keyId = (new PgpProxyMaker).get( options.scalaSigning.forceSigningExternally.getOrElse(false) ).keyId( keyContent, key, coursierCache, logger, options.jvm, options.coursier, options.scalaSigning.cliOptions() ) .orExit(logger) if (keyId.isEmpty) if (options.force) { if (logger.verbosity >= 0) System.err.println( s"Warning: $key doesn't look like a PGP public key, proceeding anyway." ) } else { System.err.println( s"Error: $key doesn't look like a PGP public key. " + "Use --force to force uploading it anyway." ) sys.exit(1) } val res = KeyServer.add( keyContent, keyServerUri, backend ) res match { case Left(error) => System.err.println(s"Error uploading key to $keyServerUri.") if (logger.verbosity >= 0) System.err.println(s"Server response: $error") sys.exit(1) case Right(_) => val name = if (keyId.isEmpty) key else "0x" + keyId.stripPrefix("0x") logger.message(s"Key $name uploaded to $keyServerUri") } } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/pgp/PgpPushOptions.scala ================================================ package scala.cli.commands.pgp import caseapp.* import scala.cli.commands.shared.* // format: off final case class PgpPushOptions( @Recurse global: GlobalOptions = GlobalOptions(), @Recurse shared: SharedPgpPushPullOptions = SharedPgpPushPullOptions(), @Recurse coursier: CoursierOptions = CoursierOptions(), @Recurse jvm: SharedJvmOptions = SharedJvmOptions(), @Recurse scalaSigning: PgpScalaSigningOptions = PgpScalaSigningOptions(), @Group(HelpGroup.PGP.toString) @HelpMessage("Try to push the key even if Scala CLI thinks it's not a public key") @ExtraName("f") force: Boolean = false, @Group(HelpGroup.PGP.toString) @HelpMessage("Whether to exit with code 0 if no key is passed") allowEmpty: Boolean = false, ) extends HasGlobalOptions // format: on object PgpPushOptions { implicit lazy val parser: Parser[PgpPushOptions] = Parser.derive implicit lazy val help: Help[PgpPushOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/pgp/PgpScalaSigningOptions.scala ================================================ package scala.cli.commands.pgp import caseapp.* import scala.build.internal.Constants import scala.build.options as bo import scala.cli.commands.shared.HelpGroup import scala.cli.commands.tags // format: off final case class PgpScalaSigningOptions( @Group(HelpGroup.Signing.toString) @Tag(tags.restricted) @HelpMessage(s"scala-cli-signing version when running externally (${Constants.scalaCliSigningVersion} by default)") @Hidden signingCliVersion: Option[String] = None, @Group(HelpGroup.Signing.toString) @Tag(tags.restricted) @HelpMessage("Pass arguments to the Java command when running scala-cli-singing externally on JVM") @ValueDescription("option") @Hidden signingCliJavaArg: List[String] = Nil, @Group(HelpGroup.Signing.toString) @HelpMessage("When running Scala CLI on the JVM, force running scala-cli-singing externally") @Hidden @Tag(tags.restricted) forceSigningExternally: Option[Boolean] = None, @Group(HelpGroup.Signing.toString) @Tag(tags.restricted) @HelpMessage("When running Scala CLI on the JVM, force running scala-cli-singing using a native launcher or a JVM launcher") @Hidden forceJvmSigningCli: Option[Boolean] = None ) { // format: on def cliOptions(): bo.ScalaSigningCliOptions = bo.ScalaSigningCliOptions( javaArgs = signingCliJavaArg, forceExternal = forceSigningExternally, forceJvm = forceJvmSigningCli, signingCliVersion = signingCliVersion ) } object PgpScalaSigningOptions { implicit lazy val parser: Parser[PgpScalaSigningOptions] = Parser.derive implicit lazy val help: Help[PgpScalaSigningOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/pgp/PgpSign.scala ================================================ package scala.cli.commands.pgp import caseapp.core.RemainingArgs import scala.cli.signing.commands.{PgpSign as OriginalPgpSign, PgpSignOptions} object PgpSign extends PgpCommand[PgpSignOptions] { override def names = PgpCommandNames.pgpSign override def run(options: PgpSignOptions, args: RemainingArgs): Unit = OriginalPgpSign.run(options, args) } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/pgp/PgpSignExternal.scala ================================================ package scala.cli.commands.pgp import scala.cli.signing.commands.PgpSignOptions class PgpSignExternal extends PgpExternalCommand { override def hidden = true def actualHelp = PgpSignOptions.help def externalCommand = Seq("pgp", "sign") override def names = PgpCommandNames.pgpSign } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/pgp/PgpVerify.scala ================================================ package scala.cli.commands.pgp import caseapp.core.RemainingArgs import scala.cli.signing.commands.{PgpVerify as OriginalPgpVerify, PgpVerifyOptions} object PgpVerify extends PgpCommand[PgpVerifyOptions] { override def names = PgpCommandNames.pgpVerify override def run(options: PgpVerifyOptions, args: RemainingArgs): Unit = OriginalPgpVerify.run(options, args) } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/pgp/PgpVerifyExternal.scala ================================================ package scala.cli.commands.pgp import scala.cli.signing.commands.PgpVerifyOptions class PgpVerifyExternal extends PgpExternalCommand { override def hidden = true def actualHelp = PgpVerifyOptions.help def externalCommand = Seq("pgp", "verify") override def names = PgpCommandNames.pgpVerify } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/pgp/SharedPgpPushPullOptions.scala ================================================ package scala.cli.commands.pgp import caseapp.* import sttp.model.Uri import scala.build.Logger import scala.cli.commands.shared.HelpGroup import scala.cli.commands.tags // format: off final case class SharedPgpPushPullOptions( @Group(HelpGroup.PGP.toString) @HelpMessage("Key server to push / pull keys from") @ValueDescription("URL") @Tag(tags.restricted) @Tag(tags.inShortHelp) keyServer: List[String] = Nil ) { // format: on def keyServerUriOptOrExit(logger: Logger): Option[Uri] = keyServer .filter(_.trim.nonEmpty) .lastOption .map { addr => Uri.parse(addr) match { case Left(err) => if (logger.verbosity >= 0) System.err.println(s"Error parsing key server address '$addr': $err") sys.exit(1) case Right(uri) => uri } } } object SharedPgpPushPullOptions { implicit lazy val parser: Parser[SharedPgpPushPullOptions] = Parser.derive implicit lazy val help: Help[SharedPgpPushPullOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/publish/ConfigUtil.scala ================================================ package scala.cli.commands.publish import scala.build.errors.ConfigDbException object ConfigUtil { extension [T](sec: scala.cli.signing.shared.Secret[T]) { def toConfig: scala.cli.config.Secret[T] = scala.cli.config.Secret(sec.value) } extension [T](sec: scala.cli.config.Secret[T]) { def toCliSigning: scala.cli.signing.shared.Secret[T] = scala.cli.signing.shared.Secret(sec.value) } extension (opt: scala.cli.signing.shared.PasswordOption) { def toConfig: scala.cli.config.PasswordOption = opt match { case v: scala.cli.signing.shared.PasswordOption.Value => scala.cli.config.PasswordOption.Value(v.value.toConfig) case v: scala.cli.signing.shared.PasswordOption.Env => scala.cli.config.PasswordOption.Env(v.name) case v: scala.cli.signing.shared.PasswordOption.File => scala.cli.config.PasswordOption.File(v.path.toNIO) case v: scala.cli.signing.shared.PasswordOption.Command => scala.cli.config.PasswordOption.Command(v.command) } } extension (opt: scala.cli.config.PasswordOption) { def toCliSigning: scala.cli.signing.shared.PasswordOption = opt match { case v: scala.cli.config.PasswordOption.Value => scala.cli.signing.shared.PasswordOption.Value(v.value.toCliSigning) case v: scala.cli.config.PasswordOption.Env => scala.cli.signing.shared.PasswordOption.Env(v.name) case v: scala.cli.config.PasswordOption.File => scala.cli.signing.shared.PasswordOption.File(os.Path(v.path, os.pwd)) case v: scala.cli.config.PasswordOption.Command => scala.cli.signing.shared.PasswordOption.Command(v.command) } } extension [T](value: Either[Exception, T]) { def wrapConfigException: Either[ConfigDbException, T] = value.left.map(ConfigDbException(_)) } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/publish/GitRepo.scala ================================================ package scala.cli.commands.publish import org.eclipse.jgit.api.Git import scala.build.Logger import scala.jdk.CollectionConverters.* import scala.util.{Properties, Using} object GitRepo { private lazy val user = os.owner(os.home) private def trusted(path: os.Path): Boolean = if (Properties.isWin) path.toIO.canWrite() else os.owner(path) == user def gitRepoOpt(workspace: os.Path): Option[os.Path] = if (trusted(workspace)) if (os.isDir(workspace / ".git")) Some(workspace) else if (workspace.segmentCount > 0) gitRepoOpt(workspace / os.up) else None else None def ghRepoOrgName( workspace: os.Path, logger: Logger ): Either[GitRepoError, (String, String)] = gitRepoOpt(workspace) match { case Some(repo) => remotes(repo, logger) match { case Seq() => Left(new GitRepoError(s"Cannot determine GitHub organization and name for $workspace")) case Seq((_, orgName)) => Right(orgName) case more => val map = more.toMap map.get("upstream").orElse(map.get("origin")).toRight { new GitRepoError( s"Cannot determine default GitHub organization and name for $workspace" ) } } case None => Left(new GitRepoError(s"$workspace is not in a git repository")) } private def remotes(repo: os.Path, logger: Logger): Seq[(String, (String, String))] = { val remoteList = Using.resource(Git.open(repo.toIO)) { git => git.remoteList().call().asScala } logger.debug(s"Found ${remoteList.length} remotes in Git repo $repo") remoteList .iterator .flatMap { remote => val name = remote.getName remote .getURIs .asScala .iterator .map(_.toASCIIString) .flatMap(maybeGhOrgName) .map((name, _)) } .toVector } def maybeGhRepoOrgName( workspace: os.Path, logger: Logger ): Option[(String, String)] = gitRepoOpt(workspace).flatMap { repo => remotes(repo, logger) match { case Seq() => logger.debug(s"No GitHub remote found in $workspace") None case Seq((_, orgName)) => Some(orgName) case more => val map = more.toMap val res = map.get("upstream").orElse(map.get("origin")) if (res.isEmpty) new GitRepoError( s"Cannot determine default GitHub organization and name for $workspace" ) res } } def maybeGhOrgName(uri: String): Option[(String, String)] = { val httpsPattern = "https://github.com/(.+)/(.+).git".r val sshPattern = "git@github.com:(.+)/(.+).git".r uri match { case httpsPattern(org, name) => Some((org, name)) case sshPattern(org, name) => Some((org, name)) case _ => None } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/publish/GitRepoError.scala ================================================ package scala.cli.commands.publish import scala.build.errors.BuildException final class GitRepoError(message: String) extends BuildException(message) ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/publish/Ivy.scala ================================================ package scala.cli.commands.publish import coursier.core.{Configuration, MinimizedExclusions, ModuleName, Organization, Type} import coursier.publish.Pom import java.time.format.DateTimeFormatterBuilder import java.time.temporal.ChronoField import java.time.{LocalDateTime, ZoneOffset} import scala.collection.mutable import scala.xml.{Elem, NodeSeq} object Ivy { private val mavenPomNs = "http://maven.apache.org/POM/4.0.0" private def ivyLicenseNodes(license: Option[Pom.License]): NodeSeq = license match { case None => NodeSeq.Empty case Some(l) => val u = l.url.trim if u.nonEmpty then scala.xml.NodeSeq.fromSeq(Seq()) else scala.xml.NodeSeq.fromSeq(Seq()) } private def mavenScmNodes(scm: Option[Pom.Scm]): NodeSeq = scm match { case None => NodeSeq.Empty case Some(s) => val url = s.url.trim val connection = s.connection.trim val devConn = s.developerConnection.trim val children = Seq( Option.when(url.nonEmpty)({url}), Option.when(connection.nonEmpty)({connection}), Option.when(devConn.nonEmpty)( {devConn} ) ).flatten if children.isEmpty then NodeSeq.Empty else scala.xml.NodeSeq.fromSeq(Seq({children})) } private def mavenDeveloperNodes(developers: Seq[Pom.Developer]): NodeSeq = if (developers.isEmpty) NodeSeq.Empty else { val devElems = developers.map { d => val url = d.url.trim val parts = Seq( Some({d.id}), Some({d.name}), d.mail.map(m => {m}), Option.when(url.nonEmpty)({url}) ).flatten {parts} } scala.xml.NodeSeq.fromSeq(Seq({devElems})) } private def mavenProjectNamePackagingNodes( pomProjectName: Option[String], packaging: Option[Type] ): NodeSeq = val namePart = pomProjectName.flatMap { n => val t = n.trim Option.when(t.nonEmpty)({t}) } val packagingPart = packaging.map(p => {p.value}) val parts = namePart.toSeq ++ packagingPart.toSeq if parts.isEmpty then NodeSeq.Empty else scala.xml.NodeSeq.fromSeq(parts) private lazy val dateFormatter = new DateTimeFormatterBuilder() .appendValue(ChronoField.YEAR, 4) .appendValue(ChronoField.MONTH_OF_YEAR, 2) .appendValue(ChronoField.DAY_OF_MONTH, 2) .appendValue(ChronoField.HOUR_OF_DAY, 2) .appendValue(ChronoField.MINUTE_OF_HOUR, 2) .appendValue(ChronoField.SECOND_OF_MINUTE, 2) .toFormatter /** Ivy descriptor aligned with coursier `Pom.create` metadata (license, SCM, developers, optional * name/packaging). */ def create( organization: Organization, moduleName: ModuleName, version: String, description: Option[String] = None, url: Option[String] = None, pomProjectName: Option[String] = None, packaging: Option[Type] = None, // TODO Accept full-fledged coursier.Dependency dependencies: Seq[( Organization, ModuleName, String, Option[Configuration], MinimizedExclusions )] = Nil, license: Option[Pom.License] = None, scm: Option[Pom.Scm] = None, developers: Seq[Pom.Developer] = Nil, time: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC), hasPom: Boolean = true, hasDoc: Boolean = true, hasSources: Boolean = true ): String = { val licenseXml = ivyLicenseNodes(license) val scmXml = mavenScmNodes(scm) val devXml = mavenDeveloperNodes(developers) val projectMetaXml = mavenProjectNamePackagingNodes(pomProjectName, packaging) val hasMavenMetadata = scmXml.nonEmpty || devXml.nonEmpty || projectMetaXml.nonEmpty val nodes = new mutable.ListBuffer[NodeSeq] nodes += { val desc = (description, url) match { case (Some(d), Some(u)) => Seq({d}) case (Some(d), None) => Seq({d}) case (None, Some(u)) => Seq() case (None, None) => Nil } {licenseXml} {desc} {projectMetaXml} {scmXml} {devXml} } nodes += { val docConf = if (hasDoc) Seq() else Nil val sourcesConf = if (hasSources) Seq() else Nil val pomConf = if (hasPom) Seq() else Nil {docConf} {sourcesConf} {pomConf} } nodes += { val docPub = if (hasDoc) Seq() else Nil val sourcesPub = if (hasSources) Seq() else Nil val pomPub = if (hasPom) Seq() else Nil {docPub} {sourcesPub} {pomPub} } nodes += { val depNodes = dependencies.map { case (org, name, ver, confOpt, exclusions) => val conf = confOpt.map(_.value).getOrElse("compile") val confSpec = s"$conf->default(compile)" val exclusionNodes = exclusions.data.toSet().map { case (org, module) => } {exclusionNodes} } {depNodes} } val root: Elem = if (hasMavenMetadata) {nodes.result()} else {nodes.result()} Pom.print(root) } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/publish/OptionCheck.scala ================================================ package scala.cli.commands.publish import scala.build.errors.BuildException import scala.build.options.PublishOptions as BPublishOptions /** A check for missing options in [[PublishOptions]] */ trait OptionCheck { /** The "group" of check this check belongs to, so that users can filter them */ def kind: OptionCheck.Kind /** Name of the option checked, for display / reporting purposes */ def fieldName: String /** Directive name of the option checked by this check */ def directivePath: String /** Checks whether the option value is missing */ def check(options: BPublishOptions): Boolean /** Provides a way to compute a default value for this option, along with extra directives and * GitHub secrets to be set */ def defaultValue(pubOpt: BPublishOptions): Either[BuildException, OptionCheck.DefaultValue] } object OptionCheck { /** Computes a default value for a directive * * @param getValue * computes a default value * @param extraDirectives * extra using directives to be set * @param ghSecrets * GitHub secrets to be set */ final case class DefaultValue( getValue: () => Either[BuildException, Option[String]], extraDirectives: Seq[(String, String)], ghSecrets: Seq[SetSecret] ) object DefaultValue { def simple( value: String, extraDirectives: Seq[(String, String)], ghSecrets: Seq[SetSecret] ): DefaultValue = DefaultValue( () => Right(Some(value)), extraDirectives, ghSecrets ) def empty: DefaultValue = DefaultValue( () => Right(None), Nil, Nil ) } sealed abstract class Kind extends Product with Serializable object Kind { case object Core extends Kind case object Extra extends Kind case object Repository extends Kind case object Signing extends Kind val all = Seq(Core, Extra, Repository, Signing) def parse(input: String): Option[Kind] = input match { case "core" => Some(Core) case "extra" => Some(Extra) case "repo" | "repository" => Some(Repository) case "signing" => Some(Signing) case _ => None } def parseList(input: String): Either[Seq[String], Seq[Kind]] = { val results = input.split(",").map(v => (v, parse(v))).toSeq val unrecognized = results.collect { case (v, None) => v } if (unrecognized.isEmpty) Right { results.collect { case (_, Some(kind)) => kind } } else Left(unrecognized) } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/publish/OptionChecks.scala ================================================ package scala.cli.commands.publish import coursier.cache.FileCache import coursier.util.Task import sttp.client3.* import scala.build.Logger import scala.cli.commands.publish.checks.* import scala.cli.config.ConfigDb object OptionChecks { def checks( options: PublishSetupOptions, configDb: => ConfigDb, workspace: os.Path, coursierCache: FileCache[Task], logger: Logger, backend: SttpBackend[Identity, Any] ): Seq[OptionCheck] = Seq( OrganizationCheck(options, workspace, logger), NameCheck(options, workspace, logger), ComputeVersionCheck(options, workspace, logger), RepositoryCheck(options, logger), UserCheck(options, () => configDb, workspace, logger), PasswordCheck(options, () => configDb, workspace, logger), PgpSecretKeyCheck(options, coursierCache, () => configDb, logger, backend), LicenseCheck(options, logger), UrlCheck(options, workspace, logger), ScmCheck(options, workspace, logger), DeveloperCheck(options, () => configDb, logger) ) } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala ================================================ package scala.cli.commands.publish import caseapp.core.RemainingArgs import caseapp.core.help.HelpFormat import coursier.core.{Authentication, Configuration} import coursier.publish.checksum.logger.InteractiveChecksumLogger import coursier.publish.checksum.{ChecksumType, Checksums} import coursier.publish.fileset.{FileSet, Path} import coursier.publish.signing.logger.InteractiveSignerLogger import coursier.publish.signing.{NopSigner, Signer} import coursier.publish.upload.logger.InteractiveUploadLogger import coursier.publish.upload.{DummyUpload, FileUpload, HttpURLConnectionUpload, Upload} import coursier.publish.{Content, Hooks, Pom} import java.io.{File, OutputStreamWriter} import java.net.URI import java.nio.charset.StandardCharsets import java.nio.file.Paths import java.time.{Instant, LocalDateTime, ZoneOffset} import java.util.concurrent.Executors import scala.build.* import scala.build.EitherCps.{either, value} import scala.build.Ops.* import scala.build.compiler.ScalaCompilerMaker import scala.build.errors.{BuildException, CompositeBuildException, Severity} import scala.build.input.Inputs import scala.build.internal.Util import scala.build.internal.Util.ScalaDependencyOps import scala.build.options.publish.{Developer, License, Signer as PSigner, Vcs} import scala.build.options.{ BuildOptions, ComputeVersion, ConfigMonoid, PublishContextualOptions, ScalaSigningCliOptions, Scope } import scala.cli.CurrentParams import scala.cli.commands.package0.Package as PackageCmd import scala.cli.commands.pgp.PgpScalaSigningOptions import scala.cli.commands.publish.ConfigUtil.* import scala.cli.commands.publish.PublishUtils.* import scala.cli.commands.shared.{ HelpCommandGroup, HelpGroup, MainClassOptions, SharedOptions, SharedVersionOptions } import scala.cli.commands.util.BuildCommandHelpers import scala.cli.commands.{ScalaCommand, SpecificationLevel, WatchUtil} import scala.cli.config.{ConfigDb, Keys, PasswordOption, PublishCredentials} import scala.cli.errors.{ FailedToSignFileError, InvalidSonatypePublishCredentials, MalformedChecksumsError, MissingPublishOptionError, UploadError, WrongSonatypeServerError } import scala.cli.packaging.Library import scala.cli.util.ArgHelpers.* import scala.cli.util.ConfigDbUtils import scala.cli.util.ConfigPasswordOptionHelpers.* import scala.concurrent.duration.DurationInt import scala.util.control.NonFatal object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers { override def scalaSpecificationLevel: SpecificationLevel = SpecificationLevel.EXPERIMENTAL import scala.cli.commands.shared.HelpGroup.* val primaryHelpGroups: Seq[HelpGroup] = Seq(Publishing, Signing, PGP) val hiddenHelpGroups: Seq[HelpGroup] = Seq(Scala, Java, Entrypoint, Dependency, Watch) override def helpFormat: HelpFormat = super.helpFormat .withHiddenGroups(hiddenHelpGroups) .withPrimaryGroups(primaryHelpGroups) override def group: String = HelpCommandGroup.Main.toString override def sharedOptions(options: PublishOptions): Option[SharedOptions] = Some(options.shared) override def buildOptions(options: PublishOptions): Some[BuildOptions] = Some(options.buildOptions().orExit(options.shared.logger)) def mkBuildOptions( baseOptions: BuildOptions, sharedVersionOptions: SharedVersionOptions, publishParams: PublishParamsOptions, sharedPublish: SharedPublishOptions, publishRepo: PublishRepositoryOptions, scalaSigning: PgpScalaSigningOptions, publishConnection: PublishConnectionOptions, mainClass: MainClassOptions, ivy2LocalLike: Option[Boolean] ): Either[BuildException, BuildOptions] = either { val contextualOptions = PublishContextualOptions( repository = publishRepo.publishRepository.filter(_.trim.nonEmpty), repositoryIsIvy2LocalLike = ivy2LocalLike, sourceJar = sharedPublish.withSources, docJar = sharedPublish.doc, gpgSignatureId = sharedPublish.gpgKey.map(_.trim).filter(_.nonEmpty), gpgOptions = sharedPublish.gpgOption, secretKey = publishParams.secretKey.map(_.configPasswordOptions()), secretKeyPassword = publishParams.secretKeyPassword.map(_.configPasswordOptions()), repoUser = publishRepo.user, repoPassword = publishRepo.password, repoRealm = publishRepo.realm, signer = value { sharedPublish.signer .map(Positioned.commandLine) .map(PSigner.parse) .sequence }, computeVersion = value { sharedVersionOptions.computeVersion .map(Positioned.commandLine) .map(ComputeVersion.parse) .sequence }, checksums = { val input = sharedPublish.checksum.flatMap(_.split(",")).map(_.trim).filter(_.nonEmpty) if input.isEmpty then None else Some(input) }, connectionTimeoutRetries = publishConnection.connectionTimeoutRetries, connectionTimeoutSeconds = publishConnection.connectionTimeoutSeconds, responseTimeoutSeconds = publishConnection.responseTimeoutSeconds, stagingRepoRetries = publishConnection.stagingRepoRetries, stagingRepoWaitTimeMilis = publishConnection.stagingRepoWaitTimeMilis ) baseOptions.copy( mainClass = mainClass.mainClass.filter(_.nonEmpty), notForBloopOptions = baseOptions.notForBloopOptions.copy( publishOptions = baseOptions.notForBloopOptions.publishOptions.copy( organization = publishParams.organization.map(_.trim).filter(_.nonEmpty).map(Positioned.commandLine), name = publishParams.name.map(_.trim).filter(_.nonEmpty).map(Positioned.commandLine), moduleName = publishParams.moduleName.map(_.trim).filter(_.nonEmpty).map(Positioned.commandLine), version = sharedVersionOptions.projectVersion.map(_.trim).filter(_.nonEmpty).map( Positioned.commandLine ), url = publishParams.url.map(_.trim).filter(_.nonEmpty).map(Positioned.commandLine), license = value { publishParams.license .map(_.trim).filter(_.nonEmpty) .map(Positioned.commandLine) .map(License.parse) .sequence }, versionControl = value { publishParams.vcs .map(_.trim).filter(_.nonEmpty) .map(Positioned.commandLine) .map(Vcs.parse) .sequence }, description = publishParams.description.map(_.trim).filter(_.nonEmpty), developers = value { publishParams.developer .filter(_.trim.nonEmpty) .map(Positioned.commandLine) .map(Developer.parse) .sequence .left.map(CompositeBuildException(_)) }, scalaVersionSuffix = sharedPublish.scalaVersionSuffix.map(_.trim), scalaPlatformSuffix = sharedPublish.scalaPlatformSuffix.map(_.trim), local = ConfigMonoid.sum(Seq( baseOptions.notForBloopOptions.publishOptions.local, if publishParams.isCi then PublishContextualOptions() else contextualOptions )), ci = ConfigMonoid.sum(Seq( baseOptions.notForBloopOptions.publishOptions.ci, if publishParams.isCi then contextualOptions else PublishContextualOptions() )), signingCli = ScalaSigningCliOptions( signingCliVersion = scalaSigning.signingCliVersion, forceExternal = scalaSigning.forceSigningExternally, forceJvm = scalaSigning.forceJvmSigningCli, javaArgs = scalaSigning.signingCliJavaArg ) ) ) ) } def maybePrintLicensesAndExit(params: PublishParamsOptions): Unit = if params.license.contains("list") then { for (l <- scala.build.internal.Licenses.list) println(s"${l.id}: ${l.name} (${l.url})") sys.exit(0) } def maybePrintChecksumsAndExit(options: SharedPublishOptions): Unit = if options.checksum.contains("list") then { for (t <- ChecksumType.all) println(t.name) sys.exit(0) } override def runCommand(options: PublishOptions, args: RemainingArgs, logger: Logger): Unit = { maybePrintLicensesAndExit(options.publishParams) maybePrintChecksumsAndExit(options.sharedPublish) val baseOptions = buildOptionsOrExit(options) val inputs = options.shared.inputs(args.all).orExit(logger) CurrentParams.workspaceOpt = Some(inputs.workspace) val initialBuildOptions = mkBuildOptions( baseOptions = baseOptions, sharedVersionOptions = options.shared.sharedVersionOptions, publishParams = options.publishParams, sharedPublish = options.sharedPublish, publishRepo = options.publishRepo, scalaSigning = options.signingCli, publishConnection = options.connectionOptions, mainClass = options.mainClass, ivy2LocalLike = options.ivy2LocalLike ).orExit(logger) val threads = BuildThreads.create() val compilerMaker = options.shared.compilerMaker(threads) val docCompilerMakerOpt = options.sharedPublish.docCompilerMakerOpt val cross = options.compileCross.cross.getOrElse(false) lazy val configDb = ConfigDbUtils.configDb.orExit(logger) lazy val workingDir = options.sharedPublish.workingDir .filter(_.trim.nonEmpty) .map(os.Path(_, os.pwd)) .getOrElse { os.temp.dir( prefix = "scala-cli-publish-", deleteOnExit = true ) } val ivy2HomeOpt = options.sharedPublish.ivy2Home .filter(_.trim.nonEmpty) .map(os.Path(_, os.pwd)) doRun( inputs, logger, initialBuildOptions, compilerMaker, docCompilerMakerOpt, cross, workingDir, ivy2HomeOpt, publishLocal = false, m2Local = false, m2HomeOpt = None, forceSigningExternally = options.signingCli.forceSigningExternally.getOrElse(false), parallelUpload = options.parallelUpload, options.watch.watch, isCi = options.publishParams.isCi, () => configDb, options.mainClass, dummy = options.sharedPublish.dummy, buildTests = options.shared.scope.test.getOrElse(false) ) } /** Build artifacts */ def doRun( inputs: Inputs, logger: Logger, initialBuildOptions: BuildOptions, compilerMaker: ScalaCompilerMaker, docCompilerMaker: Option[ScalaCompilerMaker], cross: Boolean, workingDir: => os.Path, ivy2HomeOpt: Option[os.Path], publishLocal: Boolean, m2Local: Boolean = false, m2HomeOpt: Option[os.Path] = None, forceSigningExternally: Boolean, parallelUpload: Option[Boolean], watch: Boolean, isCi: Boolean, configDb: () => ConfigDb, mainClassOptions: MainClassOptions, dummy: Boolean, buildTests: Boolean ): Unit = { val actionableDiagnostics = configDb().get(Keys.actions).getOrElse(None) if watch then { val watcher = Build.watch( inputs = inputs, options = initialBuildOptions, compilerMaker = compilerMaker, docCompilerMakerOpt = docCompilerMaker, logger = logger, crossBuilds = cross, buildTests = buildTests, partial = None, actionableDiagnostics = actionableDiagnostics, postAction = () => WatchUtil.printWatchMessage() ) { _.orReport(logger).foreach { builds => maybePublish( builds = builds, workingDir = workingDir, ivy2HomeOpt = ivy2HomeOpt, publishLocal = publishLocal, m2Local = m2Local, m2HomeOpt = m2HomeOpt, logger = logger, allowExit = false, forceSigningExternally = forceSigningExternally, parallelUpload = parallelUpload, isCi = isCi, configDb = configDb, mainClassOptions = mainClassOptions, withTestScope = buildTests, dummy = dummy ) } } try WatchUtil.waitForCtrlC(() => watcher.schedule()) finally watcher.dispose() } else { val builds = Build.build( inputs = inputs, options = initialBuildOptions, compilerMaker = compilerMaker, docCompilerMakerOpt = docCompilerMaker, logger = logger, crossBuilds = cross, buildTests = buildTests, partial = None, actionableDiagnostics = actionableDiagnostics ).orExit(logger) maybePublish( builds = builds, workingDir = workingDir, ivy2HomeOpt = ivy2HomeOpt, publishLocal = publishLocal, m2Local = m2Local, m2HomeOpt = m2HomeOpt, logger = logger, allowExit = true, forceSigningExternally = forceSigningExternally, parallelUpload = parallelUpload, isCi = isCi, configDb = configDb, mainClassOptions = mainClassOptions, withTestScope = buildTests, dummy = dummy ) } } /** Check if all builds are successful and proceed with preparing files to be uploaded OR print * main classes if the option is specified */ private def maybePublish( builds: Builds, workingDir: os.Path, ivy2HomeOpt: Option[os.Path], publishLocal: Boolean, m2Local: Boolean, m2HomeOpt: Option[os.Path], logger: Logger, allowExit: Boolean, forceSigningExternally: Boolean, parallelUpload: Option[Boolean], isCi: Boolean, configDb: () => ConfigDb, mainClassOptions: MainClassOptions, withTestScope: Boolean, dummy: Boolean ): Unit = { val allOk = builds.all.forall { case _: Build.Successful => true case _: Build.Cancelled => false case _: Build.Failed => false } if allOk then logger.log("All standard builds ok...") else { val failedBuilds = builds.all.filterNot(_.success) val cancelledBuilds = builds.all.filter(_.cancelled) logger.log( s"Some standard builds were not successful (${failedBuilds.length} failed, ${cancelledBuilds.length} cancelled)." ) } val allDocsOk = builds.allDoc.forall { case _: Build.Successful => true case _: Build.Cancelled => true case _: Build.Failed => false } if allDocsOk then logger.log("All doc builds ok...") else { val failedBuilds = builds.allDoc.filterNot(_.success) val cancelledBuilds = builds.allDoc.filter(_.cancelled) logger.log( s"Some doc builds were not successful (${failedBuilds.length} failed, ${cancelledBuilds.length} cancelled)." ) } if allOk && allDocsOk then { val builds0 = builds.all.collect { case s: Build.Successful => s } val docBuilds0 = builds.allDoc.collect { case s: Build.Successful => s } val res: Either[BuildException, Unit] = builds.builds match { case b if b.forall(_.success) && mainClassOptions.mainClassLs.contains(true) => mainClassOptions.maybePrintMainClasses( mainClasses = builds0.flatMap(_.foundMainClasses()).distinct, shouldExit = allowExit ) case _ => prepareFilesAndUpload( builds = builds0, docBuilds = docBuilds0, workingDir = workingDir, ivy2HomeOpt = ivy2HomeOpt, publishLocal = publishLocal, m2Local = m2Local, m2HomeOpt = m2HomeOpt, logger = logger, forceSigningExternally = forceSigningExternally, parallelUpload = parallelUpload, withTestScope = withTestScope, isCi = isCi, configDb = configDb, dummy = dummy ) } if allowExit then res.orExit(logger) else res.orReport(logger) } else { val msg = if allOk then "Scaladoc generation failed" else "Compilation failed" System.err.println(msg) if allowExit then sys.exit(1) } } private def buildFileSet( builds: Seq[Build.Successful], docBuilds: Seq[Build.Successful], workingDir: os.Path, now: Instant, isIvy2LocalLike: Boolean, isCi: Boolean, isSonatype: Boolean, withTestScope: Boolean, logger: Logger ): Either[BuildException, (FileSet, (coursier.core.Module, String))] = either { logger.debug(s"Preparing project ${builds.head.project.projectName}") val publishOptions = builds.head.options.notForBloopOptions.publishOptions val ArtifactData(org, moduleName, ver) = value { publishOptions.artifactData( workspace = builds.head.inputs.workspace, logger = logger, scalaArtifactsOpt = builds.head.artifacts.scalaOpt, isCi = isCi ) } logger.message(s"Publishing $org:$moduleName:$ver") val mainJar: os.Path = { val mainClassOpt: Option[String] = (builds.head.options.mainClass.filter(_.nonEmpty) match { case Some(cls) => Right(cls) case None => val potentialMainClasses = builds.flatMap(_.foundMainClasses()).distinct builds .map { build => build.retainedMainClass(logger, potentialMainClasses) .map(mainClass => build.scope -> mainClass) } .sequence .left .map(CompositeBuildException(_)) .map(_.toMap) .map { retainedMainClassesByScope => if retainedMainClassesByScope.size == 1 then retainedMainClassesByScope.head._2 else retainedMainClassesByScope .get(Scope.Main) .orElse(retainedMainClassesByScope.get(Scope.Test)) .get } }).toOption logger.debug(s"Retained main class: ${mainClassOpt.getOrElse("(no main class found)")}") val libraryJar: os.Path = Library.libraryJar(builds, mainClassOpt) val dest: os.Path = workingDir / org / s"$moduleName-$ver.jar" logger.debug(s"Copying library jar from $libraryJar to $dest...") os.copy.over(libraryJar, dest, createFolders = true) logger.log(s"Successfully copied library jar from $libraryJar to $dest") dest } val sourceJarOpt = if publishOptions.contextual(isCi).sourceJar.getOrElse(true) then { val content = PackageCmd.sourceJar(builds, now.toEpochMilli) val sourceJar: os.Path = workingDir / org / s"$moduleName-$ver-sources.jar" logger.debug(s"Saving source jar to $sourceJar...") os.write.over(sourceJar, content, createFolders = true) logger.log(s"Successfully saved source jar to $sourceJar") Some(sourceJar) } else None val docJarOpt = if publishOptions.contextual(isCi).docJar.getOrElse(true) then docBuilds match { case Nil => None case docBuilds => val docJarPath: os.Path = value(PackageCmd.docJar( builds = docBuilds, logger = logger, extraArgs = Nil, withTestScope = withTestScope )) val docJar: os.Path = workingDir / org / s"$moduleName-$ver-javadoc.jar" logger.debug(s"Copying doc jar from $docJarPath to $docJar...") os.copy.over(docJarPath, docJar, createFolders = true) logger.log(s"Successfully copied doc jar from $docJarPath to $docJar") Some(docJar) } else None val dependencies = builds.flatMap(_.artifacts.userDependencies) .map(_.toCs(builds.head.artifacts.scalaOpt.map(_.params))) .sequence .left.map(CompositeBuildException(_)) .orExit(logger) .map { dep0 => val config = builds -> builds.length match { case (b, 1) if b.head.scope != Scope.Main => Some(Configuration(b.head.scope.name)) case _ => None } logger.debug( s"Dependency ${dep0.module.organization}:${dep0.module.name}:${dep0.versionConstraint.asString}" ) ( dep0.module.organization, dep0.module.name, dep0.versionConstraint.asString, config, dep0.minimizedExclusions ) } val url = publishOptions.url.map(_.value) logger.debug(s"Published project URL: ${url.getOrElse("(not set)")}") val license = publishOptions.license.map(_.value).map { l => Pom.License(l.name, l.url) } logger.debug(s"Published project license: ${license.map(_.name).getOrElse("(not set)")}") val scm = publishOptions.versionControl.map { vcs => Pom.Scm(vcs.url, vcs.connection, vcs.developerConnection) } logger.debug(s"Published project SCM: ${scm.map(_.url).getOrElse("(not set)")}") val developers = publishOptions.developers.map { dev => Pom.Developer(dev.id, dev.name, dev.url, dev.mail) } logger.debug(s"Published project developers: ${developers.map(_.name).mkString(", ")}") val description = publishOptions.description.getOrElse(moduleName) logger.debug(s"Published project description: $description") val pomProjectName = publishOptions.pomProjectNameForMaven(moduleName) val pomContent = Pom.create( organization = coursier.Organization(org), moduleName = coursier.ModuleName(moduleName), version = ver, packaging = None, url = url, name = Some(pomProjectName), dependencies = dependencies, description = Some(description), license = license, scm = scm, developers = developers ) if isSonatype then { if url.isEmpty then logger.diagnostic( "Publishing to Sonatype, but project URL is empty (set it with the '//> using publish.url' directive)." ) if license.isEmpty then logger.diagnostic( "Publishing to Sonatype, but license is empty (set it with the '//> using publish.license' directive)." ) if scm.isEmpty then logger.diagnostic( "Publishing to Sonatype, but SCM details are empty (set them with the '//> using publish.scm' directive)." ) if developers.isEmpty then logger.diagnostic( "Publishing to Sonatype, but developer details are empty (set them with the '//> using publish.developer' directive)." ) } def ivyContent = Ivy.create( organization = coursier.Organization(org), moduleName = coursier.ModuleName(moduleName), version = ver, url = url, pomProjectName = Some(pomProjectName), dependencies = dependencies, description = Some(description), license = license, scm = scm, developers = developers, time = LocalDateTime.ofInstant(now, ZoneOffset.UTC), hasDoc = docJarOpt.isDefined, hasSources = sourceJarOpt.isDefined ) def mavenFileSet: FileSet = { val basePath = Path(org.split('.').toSeq ++ Seq(moduleName, ver)) val mainEntries = Seq( (basePath / s"$moduleName-$ver.pom") -> Content.InMemory( now, pomContent.getBytes(StandardCharsets.UTF_8) ), (basePath / s"$moduleName-$ver.jar") -> Content.File(mainJar.toNIO) ) val sourceJarEntries = sourceJarOpt .map { sourceJar => (basePath / s"$moduleName-$ver-sources.jar") -> Content.File(sourceJar.toNIO) } .toSeq val docJarEntries = docJarOpt .map { docJar => (basePath / s"$moduleName-$ver-javadoc.jar") -> Content.File(docJar.toNIO) } .toSeq // TODO version listings, … FileSet(mainEntries ++ sourceJarEntries ++ docJarEntries) } def ivy2LocalLikeFileSet: FileSet = { val basePath = Path(Seq(org, moduleName, ver)) val mainEntries = Seq( (basePath / "poms" / s"$moduleName.pom") -> Content.InMemory( now, pomContent.getBytes(StandardCharsets.UTF_8) ), (basePath / "ivys" / "ivy.xml") -> Content.InMemory( now, ivyContent.getBytes(StandardCharsets.UTF_8) ), (basePath / "jars" / s"$moduleName.jar") -> Content.File(mainJar.toNIO) ) val sourceJarEntries = sourceJarOpt .map { sourceJar => (basePath / "srcs" / s"$moduleName-sources.jar") -> Content.File(sourceJar.toNIO) } .toSeq val docJarEntries = docJarOpt .map { docJar => (basePath / "docs" / s"$moduleName-javadoc.jar") -> Content.File(docJar.toNIO) } .toSeq FileSet(mainEntries ++ sourceJarEntries ++ docJarEntries) } val fileSet: FileSet = if isIvy2LocalLike then ivy2LocalLikeFileSet else mavenFileSet val mod = coursier.core.Module( coursier.core.Organization(org), coursier.core.ModuleName(moduleName), Map() ) (fileSet, (mod, ver)) } /** Sign and checksum files, then upload everything to the target repository */ private def prepareFilesAndUpload( builds: Seq[Build.Successful], docBuilds: Seq[Build.Successful], workingDir: os.Path, ivy2HomeOpt: Option[os.Path], publishLocal: Boolean, m2Local: Boolean, m2HomeOpt: Option[os.Path], logger: Logger, forceSigningExternally: Boolean, parallelUpload: Option[Boolean], withTestScope: Boolean, isCi: Boolean, configDb: () => ConfigDb, dummy: Boolean ): Either[BuildException, Unit] = either { assert(docBuilds.isEmpty || docBuilds.length == builds.length) val groupedBuilds = builds.groupedByCrossParams val groupedDocBuilds = docBuilds.groupedByCrossParams val it: Iterator[(Seq[Build.Successful], Seq[Build.Successful])] = groupedBuilds.keysIterator.map { key => (groupedBuilds(key), groupedDocBuilds.getOrElse(key, Seq.empty)) } val publishOptions = ConfigMonoid.sum( builds.map(_.options.notForBloopOptions.publishOptions) ) val ec = builds.head.options.finalCache.ec def authOpt( repo: String, isLegacySonatype: Boolean ): Either[BuildException, Option[Authentication]] = either { val publishCredentials: () => Option[PublishCredentials] = () => value(PublishUtils.getPublishCredentials(repo, configDb)) for { password <- publishOptions.contextual(isCi).repoPassword .map(_.toConfig) .orElse(publishCredentials().flatMap(_.password)) .map(_.get().value) user = publishOptions.contextual(isCi).repoUser .map(_.toConfig) .orElse(publishCredentials().flatMap(_.user)) .map(_.get().value) .getOrElse("") auth = Authentication(user, password) } yield publishOptions.contextual(isCi).repoRealm .orElse { publishCredentials() .flatMap(_.realm) .orElse(if isLegacySonatype then Some("Sonatype Nexus Repository Manager") else None) } .map(auth.withRealm) .getOrElse(auth) } val repoParams: RepoParams = { lazy val es = Executors.newSingleThreadScheduledExecutor(Util.daemonThreadFactory("publish-retry")) if publishLocal && m2Local then RepoParams.m2Local(m2HomeOpt) else if publishLocal then RepoParams.ivy2Local(ivy2HomeOpt) else value { publishOptions.contextual(isCi).repository match { case None => Left(MissingPublishOptionError.repositoryError) case Some(repo) => RepoParams( repo = repo, vcsUrlOpt = publishOptions.versionControl.map(_.url), workspace = builds.head.inputs.workspace, ivy2HomeOpt = ivy2HomeOpt, isIvy2LocalLike = publishOptions.contextual(isCi).repositoryIsIvy2LocalLike.getOrElse(false), es = es, logger = logger, connectionTimeoutRetries = publishOptions.contextual(isCi).connectionTimeoutRetries, connectionTimeoutSeconds = publishOptions.contextual(isCi).connectionTimeoutSeconds, stagingRepoRetries = publishOptions.contextual(isCi).stagingRepoRetries, stagingRepoWaitTimeMilis = publishOptions.contextual(isCi).stagingRepoWaitTimeMilis ) } } } val now = Instant.now() val (fileSet0, modVersionOpt) = value { it .map { case (builds, docBuilds) => buildFileSet( builds = builds, docBuilds = docBuilds, workingDir = workingDir, now = now, isIvy2LocalLike = repoParams.isIvy2LocalLike, isCi = isCi, isSonatype = repoParams.isSonatype, withTestScope = withTestScope, logger = logger ) } .sequence0 .map { l => val fs = l.map(_._1).foldLeft(FileSet.empty)(_ ++ _) val modVersionOpt0 = l.headOption.map(_._2) (fs, modVersionOpt0) } } def getBouncyCastleSigner( secretKey: PasswordOption, secretKeyPasswordOpt: Option[PasswordOption] ) = PublishUtils.getBouncyCastleSigner( secretKey = secretKey, secretKeyPasswordOpt = secretKeyPasswordOpt, buildOptions = builds.headOption.map(_.options), forceSigningExternally = forceSigningExternally, logger = logger ) val signerKind: PSigner = publishOptions.contextual(isCi).signer.getOrElse { if !repoParams.supportsSig then PSigner.Nop else if publishOptions.contextual(isCi).gpgSignatureId.isDefined then PSigner.Gpg else if repoParams.shouldSign || publishOptions.contextual(isCi).secretKey.isDefined then PSigner.BouncyCastle else PSigner.Nop } def getSecretKeyPasswordOpt: Option[PasswordOption] = publishOptions.contextual(isCi).getSecretKeyPasswordOpt(configDb) val signer: Either[BuildException, Signer] = signerKind match { // user specified --signer=gpg or --gpgKey=... case PSigner.Gpg => publishOptions.contextual(isCi).getGpgSigner // user specified --signer=bc or --secret-key=... or target repository requires signing // --secret-key-password is possibly specified (not mandatory) case PSigner.BouncyCastle if publishOptions.contextual(isCi).secretKey.isDefined => val secretKeyConfigOpt = publishOptions.contextual(isCi).secretKey.get for { secretKey <- secretKeyConfigOpt.get(configDb()) } yield getBouncyCastleSigner( secretKey = secretKey, secretKeyPasswordOpt = getSecretKeyPasswordOpt ) // user specified --signer=bc or target repository requires signing // --secret-key-password is possibly specified (not mandatory) case PSigner.BouncyCastle => val shouldSignMsg = if repoParams.shouldSign then "signing is required for chosen repository" else "" for { secretKeyOpt <- configDb().get(Keys.pgpSecretKey).wrapConfigException secretKey <- secretKeyOpt.toRight( new MissingPublishOptionError( name = "secret key", optionName = "--secret-key", directiveName = "", configKeys = Seq(Keys.pgpSecretKey.fullName), extraMessage = shouldSignMsg ) ) } yield getBouncyCastleSigner(secretKey, getSecretKeyPasswordOpt) case _ => if !publishOptions.contextual(isCi).signer.contains(PSigner.Nop) then logger.message( " \ud83d\udd13 Artifacts NOT signed as it's not required nor has it been specified" ) Right(NopSigner) } val signerLogger = new InteractiveSignerLogger(out = new OutputStreamWriter(System.err), verbosity = 1) val signRes: Either[(Path, Content, String), FileSet] = value(signer).signatures( fileSet = fileSet0, now = now, dontSignExtensions = ChecksumType.all.map(_.extension).toSet, dontSignFiles = Set("maven-metadata.xml"), logger = signerLogger ) val fileSet1 = value { signRes .left.map { case (path, content, err) => val path0 = content.pathOpt .map(os.Path(_, Os.pwd)) .toRight(path.repr) new FailedToSignFileError(path0, err) } .map { signatures => fileSet0 ++ signatures } } val checksumLogger = new InteractiveChecksumLogger(new OutputStreamWriter(System.err), verbosity = 1) val checksumTypes = publishOptions.contextual(isCi).checksums match { case None => if repoParams.acceptsChecksums then Seq(ChecksumType.MD5, ChecksumType.SHA1) else Nil case Some(Seq("none")) => Nil case Some(inputs) => value { inputs .map(ChecksumType.parse) .sequence .left.map(errors => new MalformedChecksumsError(inputs, errors)) } } val checksums = Checksums( types = checksumTypes, fileSet = fileSet1, now = now, pool = ec, logger = checksumLogger ).unsafeRun()(using ec) val fileSet2 = fileSet1 ++ checksums val finalFileSet = if repoParams.isIvy2LocalLike then fileSet2 else fileSet2.order(ec).unsafeRun()(using ec) val isSnapshot0 = modVersionOpt.exists(_._2.endsWith("SNAPSHOT")) if isSnapshot0 then logger.message("Publishing a SNAPSHOT version...") val authOpt0: Option[Authentication] = value(authOpt( repo = repoParams.repo.repo(isSnapshot0).root, isLegacySonatype = repoParams.isSonatype )) val asciiRegex = """[\u0000-\u007f]*""".r val usernameOnlyAscii = authOpt0.exists(_.userOpt.exists(asciiRegex.matches)) val passwordOnlyAscii = authOpt0.exists(_.passwordOpt.exists(asciiRegex.matches)) if repoParams.shouldAuthenticate && authOpt0.isEmpty then logger.diagnostic( "Publishing to a repository that needs authentication, but no credentials are available.", Severity.Warning ) val repoParams0: RepoParams = repoParams.withAuth(authOpt0) val isLegacySonatype = repoParams0.isSonatype && !repoParams0.repo.releaseRepo.root.contains("s01") val hooksDataOpt = Option.when(!dummy) { try repoParams0.hooks.beforeUpload(finalFileSet, isSnapshot0).unsafeRun()(using ec) catch { case NonFatal(e) if "Failed to get .*oss\\.sonatype\\.org.*/staging/profiles \\(http status: 403,".r .unanchored.matches( e.getMessage ) => logger.exit(new WrongSonatypeServerError(isLegacySonatype)) case NonFatal(e) if "Failed to get .*oss\\.sonatype\\.org.*/staging/profiles \\(http status: 401,".r .unanchored.matches( e.getMessage ) => logger.exit(new InvalidSonatypePublishCredentials(usernameOnlyAscii, passwordOnlyAscii)) case NonFatal(e) => throw new Exception(e) } } val retainedRepo = hooksDataOpt match { case None => // dummy mode repoParams0.repo.repo(isSnapshot0) case Some(hooksData) => repoParams0.hooks.repository(hooksData, repoParams0.repo, isSnapshot0) .getOrElse(repoParams0.repo.repo(isSnapshot0)) } val baseUpload = if retainedRepo.root.startsWith("http://") || retainedRepo.root.startsWith("https://") then HttpURLConnectionUpload.create( publishOptions.contextual(isCi) .connectionTimeoutSeconds.map(_.seconds.toMillis.toInt), publishOptions.contextual(isCi) .responseTimeoutSeconds.map(_.seconds.toMillis.toInt) ) else FileUpload(Paths.get(new URI(retainedRepo.root))) val upload = if dummy then DummyUpload(baseUpload) else baseUpload val isLocal = true val uploadLogger = InteractiveUploadLogger.create(System.err, dummy = dummy, isLocal = isLocal) val errors = try upload.uploadFileSet( repository = retainedRepo, fileSet = finalFileSet, logger = uploadLogger, parallel = if parallelUpload.getOrElse(repoParams.defaultParallelUpload) then Some(ec) else None ).unsafeRun()(using ec) catch { case NonFatal(e) => // Wrap exception from coursier, as it sometimes throws exceptions from other threads, // which lack the current stacktrace. throw new Exception(e) } errors.toList match { case (h @ (_, _, e: Upload.Error.HttpError)) :: t if repoParams0.isSonatype && errors.distinctBy(_._3.getMessage()).size == 1 => logger.log(s"Error message: ${e.getMessage}") val httpCodeRegex = "HTTP (\\d+)\n.*".r e.getMessage match { case httpCodeRegex("403") => if logger.verbosity >= 2 then e.printStackTrace() logger.error( s""" |Uploading files failed! |Possible causes: |- no rights to publish under this organization |- organization name is misspelled | -> have you registered your organisation yet? |""".stripMargin ) value(Left(new UploadError(::(h, t)))) case _ => value(Left(new UploadError(::(h, t)))) } case h :: t if repoParams0.isSonatype && errors.forall { case (_, _, _: Upload.Error.Unauthorized) => true case _ => false } => logger.error( s""" |Uploading files failed! |Possible causes: |- incorrect Sonatype credentials |- incorrect Sonatype server was used, try ${ if isLegacySonatype then "'central-s01'" else "'central'" } | -> consult publish subcommand documentation |""".stripMargin ) value(Left(new UploadError(::(h, t)))) case h :: t => value(Left(new UploadError(::(h, t)))) case Nil => for (hooksData <- hooksDataOpt) try repoParams0.hooks.afterUpload(hooksData).unsafeRun()(using ec) catch { case NonFatal(e) => throw new Exception(e) } for ((mod, version) <- modVersionOpt) { val checkRepo = repoParams0.repo.checkResultsRepo(isSnapshot0) val relPath = { val elems = if repoParams.isIvy2LocalLike then Seq(mod.organization.value, mod.name.value, version) else mod.organization.value.split('.').toSeq ++ Seq(mod.name.value, version) elems.mkString("/", "/", "/") } val path = { val url = checkRepo.root.stripSuffix("/") + relPath if url.startsWith("file:") then { val path = os.Path(Paths.get(new URI(url)), os.pwd) if path.startsWith(os.pwd) then path.relativeTo(os.pwd).segments.map(_ + File.separator).mkString else if path.startsWith(os.home) then ("~" +: path.relativeTo(os.home).segments).map(_ + File.separator).mkString else path.toString } else url } if dummy then println("\n \ud83d\udc40 You could have checked results at") else println("\n \ud83d\udc40 Check results at") println(s" $path") for (targetRepo <- repoParams.targetRepoOpt if !isSnapshot0) { val url = targetRepo.stripSuffix("/") + relPath if dummy then println("before they would have landed at") else println("before they land at") println(s" $url") } } } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/publish/PublishConnectionOptions.scala ================================================ package scala.cli.commands.publish import caseapp.* import scala.cli.commands.shared.HelpGroup import scala.cli.commands.tags // format: off final case class PublishConnectionOptions( @Group(HelpGroup.Publishing.toString) @HelpMessage("Connection timeout, in seconds.") @Tag(tags.restricted) @Hidden connectionTimeoutSeconds: Option[Int] = None, @Group(HelpGroup.Publishing.toString) @HelpMessage("How many times to retry establishing the connection on timeout.") @Tag(tags.restricted) @Hidden connectionTimeoutRetries: Option[Int] = None, @Group(HelpGroup.Publishing.toString) @HelpMessage("Waiting for response timeout, in seconds.") @Tag(tags.restricted) @Hidden responseTimeoutSeconds: Option[Int] = None, @Group(HelpGroup.Publishing.toString) @HelpMessage("How many times to retry the staging repository operations on failure.") @Tag(tags.restricted) @Hidden stagingRepoRetries: Option[Int] = None, @Group(HelpGroup.Publishing.toString) @HelpMessage("Time to wait between staging repository operation retries, in milliseconds.") @Tag(tags.restricted) @Hidden stagingRepoWaitTimeMilis: Option[Int] = None ) // format: on object PublishConnectionOptions { implicit lazy val parser: Parser[PublishConnectionOptions] = Parser.derive implicit lazy val help: Help[PublishConnectionOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/publish/PublishLocal.scala ================================================ package scala.cli.commands.publish import caseapp.core.RemainingArgs import caseapp.core.help.HelpFormat import scala.build.{BuildThreads, Logger} import scala.cli.CurrentParams import scala.cli.commands.ScalaCommand import scala.cli.commands.shared.{HelpCommandGroup, SharedOptions} import scala.cli.config.ConfigDb import scala.cli.util.ArgHelpers.* object PublishLocal extends ScalaCommand[PublishLocalOptions] { override def group: String = HelpCommandGroup.Main.toString override def scalaSpecificationLevel = SpecificationLevel.EXPERIMENTAL override def helpFormat: HelpFormat = super.helpFormat .withHiddenGroups(Publish.hiddenHelpGroups) .withPrimaryGroups(Publish.primaryHelpGroups) override def sharedOptions(options: PublishLocalOptions): Option[SharedOptions] = Some(options.shared) override def buildOptions(options: PublishLocalOptions): Some[scala.build.options.BuildOptions] = Some(options.buildOptions().orExit(options.shared.logger)) override def names: List[List[String]] = List( List("publish", "local") ) override def runCommand( options: PublishLocalOptions, args: RemainingArgs, logger: Logger ): Unit = { Publish.maybePrintLicensesAndExit(options.publishParams) Publish.maybePrintChecksumsAndExit(options.sharedPublish) if options.m2 && options.sharedPublish.ivy2Home.exists(_.trim.nonEmpty) then { logger.error("--m2 and --ivy2-home are mutually exclusive.") sys.exit(1) } val baseOptions = buildOptionsOrExit(options) val inputs = options.shared.inputs(args.all).orExit(logger) CurrentParams.workspaceOpt = Some(inputs.workspace) val initialBuildOptions = Publish.mkBuildOptions( baseOptions, options.shared.sharedVersionOptions, options.publishParams, options.sharedPublish, PublishRepositoryOptions(), options.scalaSigning, PublishConnectionOptions(), options.mainClass, None ).orExit(logger) val threads = BuildThreads.create() val compilerMaker = options.shared.compilerMaker(threads) val docCompilerMakerOpt = options.sharedPublish.docCompilerMakerOpt val cross = options.compileCross.cross.getOrElse(false) lazy val workingDir = options.sharedPublish.workingDir .filter(_.trim.nonEmpty) .map(os.Path(_, os.pwd)) .getOrElse { os.temp.dir( prefix = "scala-cli-publish-", deleteOnExit = true ) } val ivy2HomeOpt = options.sharedPublish.ivy2Home .filter(_.trim.nonEmpty) .map(os.Path(_, os.pwd)) val m2HomeOpt = options.m2Home .filter(_.trim.nonEmpty) .map(os.Path(_, os.pwd)) Publish.doRun( inputs = inputs, logger = logger, initialBuildOptions = initialBuildOptions, compilerMaker = compilerMaker, docCompilerMaker = docCompilerMakerOpt, cross = cross, workingDir = workingDir, ivy2HomeOpt = ivy2HomeOpt, publishLocal = true, m2Local = options.m2, m2HomeOpt = m2HomeOpt, forceSigningExternally = options.scalaSigning.forceSigningExternally.getOrElse(false), parallelUpload = Some(true), watch = options.watch.watch, isCi = options.publishParams.isCi, configDb = () => ConfigDb.empty, // shouldn't be used, no need of repo credentials here mainClassOptions = options.mainClass, dummy = options.sharedPublish.dummy, buildTests = options.shared.scope.test.getOrElse(false) ) } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/publish/PublishLocalOptions.scala ================================================ package scala.cli.commands.publish import caseapp.* import scala.cli.commands.pgp.PgpScalaSigningOptions import scala.cli.commands.shared.* import scala.cli.commands.tags // format: off @HelpMessage(PublishLocalOptions.helpMessage, "", PublishLocalOptions.detailedHelpMessage) final case class PublishLocalOptions( @Recurse shared: SharedOptions = SharedOptions(), @Recurse watch: SharedWatchOptions = SharedWatchOptions(), @Recurse compileCross: CrossOptions = CrossOptions(), @Recurse mainClass: MainClassOptions = MainClassOptions(), @Recurse publishParams: PublishParamsOptions = PublishParamsOptions(), @Recurse sharedPublish: SharedPublishOptions = SharedPublishOptions(), @Recurse scalaSigning: PgpScalaSigningOptions = PgpScalaSigningOptions(), @Group(HelpGroup.Publishing.toString) @HelpMessage("Publish to the local Maven repository (defaults to ~/.m2/repository) instead of Ivy2 local") @Name("mavenLocal") @Tag(tags.experimental) @Tag(tags.inShortHelp) m2: Boolean = false, @Group(HelpGroup.Publishing.toString) @HelpMessage("Set the local Maven repository path (defaults to ~/.m2/repository)") @ValueDescription("path") @Tag(tags.experimental) m2Home: Option[String] = None, ) extends HasSharedOptions with HasSharedWatchOptions // format: on object PublishLocalOptions { implicit lazy val parser: Parser[PublishLocalOptions] = Parser.derive implicit lazy val help: Help[PublishLocalOptions] = Help.derive val cmdName = "publish local" private val helpHeader = "Publishes build artifacts to the local Ivy2 or Maven repository." private val docWebsiteSuffix = "publishing/publish-local" val helpMessage: String = s"""$helpHeader | |${HelpMessages.commandFullHelpReference(cmdName)} |${HelpMessages.commandDocWebsiteReference(docWebsiteSuffix)}""".stripMargin val detailedHelpMessage: String = s"""$helpHeader | |${HelpMessages.commandDocWebsiteReference(docWebsiteSuffix)}""".stripMargin } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/publish/PublishOptions.scala ================================================ package scala.cli.commands.publish import caseapp.* import scala.cli.ScalaCli.baseRunnerName import scala.cli.commands.pgp.PgpScalaSigningOptions import scala.cli.commands.shared.* import scala.cli.commands.tags // format: off @HelpMessage(PublishOptions.helpMessage, "", PublishOptions.detailedHelpMessage) final case class PublishOptions( @Recurse shared: SharedOptions = SharedOptions(), @Recurse watch: SharedWatchOptions = SharedWatchOptions(), @Recurse compileCross: CrossOptions = CrossOptions(), @Recurse mainClass: MainClassOptions = MainClassOptions(), @Recurse publishParams: PublishParamsOptions = PublishParamsOptions(), @Recurse publishRepo: PublishRepositoryOptions = PublishRepositoryOptions(), @Recurse sharedPublish: SharedPublishOptions = SharedPublishOptions(), @Recurse signingCli: PgpScalaSigningOptions = PgpScalaSigningOptions(), @Recurse connectionOptions: PublishConnectionOptions = PublishConnectionOptions(), @Group(HelpGroup.Publishing.toString) @Tag(tags.restricted) @Hidden ivy2LocalLike: Option[Boolean] = None, @Group(HelpGroup.Publishing.toString) @Tag(tags.restricted) @Hidden parallelUpload: Option[Boolean] = None ) extends HasSharedOptions with HasSharedWatchOptions // format: on object PublishOptions { implicit lazy val parser: Parser[PublishOptions] = Parser.derive implicit lazy val help: Help[PublishOptions] = Help.derive val cmdName = "publish" private val helpHeader = "Publishes build artifacts to Maven repositories." val helpMessage: String = s"""$helpHeader | |${HelpMessages.commandFullHelpReference(cmdName, needsPower = true)} |${HelpMessages.commandDocWebsiteReference(s"publishing/$cmdName")}""".stripMargin val detailedHelpMessage: String = s"""$helpHeader | |We recommend running the `publish setup` sub-command once prior to |running `publish` in order to set missing `using` directives for publishing. |(but this is not mandatory) | $baseRunnerName --power publish setup . | |${HelpMessages.commandConfigurations(cmdName)} | |${HelpMessages.acceptedInputs} | |${HelpMessages.commandDocWebsiteReference(s"publishing/$cmdName")}""".stripMargin } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/publish/PublishParamsOptions.scala ================================================ package scala.cli.commands.publish import caseapp.* import scala.build.internals.EnvVar import scala.cli.commands.shared.HelpGroup import scala.cli.commands.tags import scala.cli.util.ArgParsers.* import scala.cli.util.MaybeConfigPasswordOption // format: off final case class PublishParamsOptions( @Group(HelpGroup.Publishing.toString) @HelpMessage("Organization to publish artifacts under") @Tag(tags.restricted) @Tag(tags.inShortHelp) organization: Option[String] = None, @Group(HelpGroup.Publishing.toString) @HelpMessage("Name to publish artifacts as") @Tag(tags.restricted) @Tag(tags.inShortHelp) name: Option[String] = None, @Group(HelpGroup.Publishing.toString) @HelpMessage("Final name to publish artifacts as, including Scala version and platform suffixes if any") @Tag(tags.restricted) @Tag(tags.inShortHelp) moduleName: Option[String] = None, @Group(HelpGroup.Publishing.toString) @HelpMessage("URL to put in publishing metadata") @Tag(tags.restricted) @Tag(tags.inShortHelp) url: Option[String] = None, @Group(HelpGroup.Publishing.toString) @HelpMessage("License to put in publishing metadata") @Tag(tags.restricted) @Tag(tags.inShortHelp) @ValueDescription("name:URL") license: Option[String] = None, @Group(HelpGroup.Publishing.toString) @HelpMessage("VCS information to put in publishing metadata") @Tag(tags.restricted) @Tag(tags.inShortHelp) vcs: Option[String] = None, @Group(HelpGroup.Publishing.toString) @HelpMessage("Description to put in publishing metadata") @Tag(tags.restricted) @Tag(tags.inShortHelp) description: Option[String] = None, @Group(HelpGroup.Publishing.toString) @HelpMessage("Developer(s) to add in publishing metadata, like \"alex|Alex|https://alex.info\" or \"alex|Alex|https://alex.info|alex@alex.me\"") @ValueDescription("id|name|URL|email") @Tag(tags.restricted) @Tag(tags.inShortHelp) developer: List[String] = Nil, @Group(HelpGroup.Publishing.toString) @HelpMessage("Secret key to use to sign artifacts with Bouncy Castle") @Tag(tags.restricted) @Tag(tags.inShortHelp) secretKey: Option[MaybeConfigPasswordOption] = None, @Group(HelpGroup.Publishing.toString) @HelpMessage("Password of secret key to use to sign artifacts with Bouncy Castle") @ValueDescription("value:…") @ExtraName("secretKeyPass") @Tag(tags.restricted) @Tag(tags.inShortHelp) secretKeyPassword: Option[MaybeConfigPasswordOption] = None, @Group(HelpGroup.Publishing.toString) @HelpMessage("Use or setup publish parameters meant to be used on continuous integration") @Tag(tags.restricted) @Tag(tags.inShortHelp) ci: Option[Boolean] = None ) { // format: on def setupCi: Boolean = ci.getOrElse(false) def isCi: Boolean = ci.getOrElse(EnvVar.Internal.ci.valueOpt.nonEmpty) } object PublishParamsOptions { implicit lazy val parser: Parser[PublishParamsOptions] = Parser.derive implicit lazy val help: Help[PublishParamsOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/publish/PublishRepositoryOptions.scala ================================================ package scala.cli.commands.publish import caseapp.* import scala.cli.commands.shared.HelpGroup import scala.cli.commands.tags import scala.cli.signing.shared.PasswordOption import scala.cli.signing.util.ArgParsers.* // format: off final case class PublishRepositoryOptions( @Group(HelpGroup.Publishing.toString) @HelpMessage("Repository to publish to") @ValueDescription("URL or path") @ExtraName("R") @ExtraName("publishRepo") @Tag(tags.restricted) @Tag(tags.inShortHelp) publishRepository: Option[String] = None, @Group(HelpGroup.Publishing.toString) @HelpMessage("User to use with publishing repository") @ValueDescription("user") @Tag(tags.restricted) @Tag(tags.inShortHelp) user: Option[PasswordOption] = None, @Group(HelpGroup.Publishing.toString) @HelpMessage("Password to use with publishing repository") @ValueDescription("value:…") @Tag(tags.restricted) @Tag(tags.inShortHelp) password: Option[PasswordOption] = None, @Group(HelpGroup.Publishing.toString) @HelpMessage("Realm to use when passing credentials to publishing repository") @ValueDescription("realm") @Tag(tags.restricted) @Tag(tags.inShortHelp) realm: Option[String] = None ) // format: on object PublishRepositoryOptions { implicit lazy val parser: Parser[PublishRepositoryOptions] = Parser.derive implicit lazy val help: Help[PublishRepositoryOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/publish/PublishSetup.scala ================================================ package scala.cli.commands.publish import caseapp.core.RemainingArgs import caseapp.core.help.HelpFormat import coursier.cache.ArchiveCache import java.nio.charset.StandardCharsets import scala.build.Ops.* import scala.build.errors.CompositeBuildException import scala.build.options.{BuildOptions, InternalOptions, Scope} import scala.build.{CrossSources, Directories, Logger, Sources} import scala.cli.ScalaCli import scala.cli.commands.github.{LibSodiumJni, SecretCreate, SecretList} import scala.cli.commands.publish.ConfigUtil.* import scala.cli.commands.shared.{HelpCommandGroup, SharedOptions} import scala.cli.commands.util.ScalaCliSttpBackend import scala.cli.commands.{CommandUtils, ScalaCommand, SpecificationLevel} import scala.cli.config.Keys import scala.cli.internal.Constants import scala.cli.util.ArgHelpers.* import scala.cli.util.ConfigDbUtils object PublishSetup extends ScalaCommand[PublishSetupOptions] { override def group: String = HelpCommandGroup.Main.toString override def scalaSpecificationLevel: SpecificationLevel = SpecificationLevel.EXPERIMENTAL override def helpFormat: HelpFormat = super.helpFormat .withHiddenGroups(Publish.hiddenHelpGroups) .withPrimaryGroups(Publish.primaryHelpGroups) override def names = List( List("publish", "setup") ) override def runCommand( options: PublishSetupOptions, args: RemainingArgs, logger: Logger ): Unit = { Publish.maybePrintLicensesAndExit(options.publishParams) val coursierCache = options.coursier.coursierCache(logger) val directories = Directories.directories lazy val configDb = ConfigDbUtils.configDb.orExit(logger) val inputArgs = args.all val inputs = { val maybeInputs = SharedOptions.inputs( inputArgs, () => None, Nil, directories, logger, coursierCache, None, options.input.defaultForbiddenDirectories, options.input.forbid, Nil, Nil, Nil, Nil ) maybeInputs match { case Left(error) => System.err.println(error) sys.exit(1) case Right(inputs0) => inputs0 } } val (pureJava, publishOptions) = { val cliBuildOptions = BuildOptions( internal = InternalOptions( cache = Some(coursierCache) ) ) val (crossSources, _) = CrossSources.forInputs( inputs, Sources.defaultPreprocessors( cliBuildOptions.archiveCache, cliBuildOptions.internal.javaClassNameVersionOpt, () => cliBuildOptions.javaHome().value.javaCommand ), logger, cliBuildOptions.suppressWarningOptions, cliBuildOptions.internal.exclude, download = cliBuildOptions.downloader ).orExit(logger) val crossSourcesSharedOptions = crossSources.sharedOptions(cliBuildOptions) val scopedSources = crossSources.scopedSources(crossSourcesSharedOptions).orExit(logger) val sources = scopedSources.sources(Scope.Main, crossSourcesSharedOptions, inputs.workspace, logger) .orExit(logger) val pureJava = sources.hasJava && !sources.hasScala (pureJava, sources.buildOptions.notForBloopOptions.publishOptions) } val backend = ScalaCliSttpBackend.httpURLConnection(logger) val checksInputOpt = options.checks.map(_.trim).filter(_.nonEmpty).filter(_ != "all") val checkKinds = checksInputOpt match { case None => OptionCheck.Kind.all.toSet case Some(checksInput) => OptionCheck.Kind.parseList(checksInput) .left.map { unrecognized => System.err.println(s"Unrecognized check(s): ${unrecognized.mkString(", ")}") sys.exit(1) } .merge .toSet } val missingFields = OptionChecks.checks(options, configDb, inputs.workspace, coursierCache, logger, backend) .filter(check => checkKinds(check.kind)) .flatMap { check => if (check.check(publishOptions)) { logger.debug(s"Found field ${check.fieldName}") Nil } else { logger.debug(s"Missing field ${check.fieldName}") Seq(check) } } if (missingFields.nonEmpty) { val count = missingFields.length logger.message(s"$count ${if (count > 1) "options need" else "option needs"} to be set") for (check <- missingFields) logger.message(s" ${check.fieldName}") logger.message("") // printing an empty line, for readability } lazy val (ghRepoOrg, ghRepoName) = GitRepo.ghRepoOrgName(inputs.workspace, logger) .orExit(logger) lazy val token = options.token .map(_.toConfig) .orElse { configDb.get(Keys.ghToken) .wrapConfigException .orExit(logger) } .map(_.get()) .getOrElse { System.err.println( s"No GitHub token passed, please specify one via --token env:ENV_VAR_NAME or --token file:/path/to/token, " + s"or by setting ${Keys.ghToken.fullName} in the config." ) sys.exit(1) } if (options.check) if (missingFields.isEmpty) logger.message("Setup fine for publishing") else { logger.message("Found missing config for publishing") sys.exit(1) } else { val missingFieldsWithDefaults = missingFields .map { check => check.defaultValue(publishOptions).map((check, _)) } .sequence .left.map(CompositeBuildException(_)) .orExit(logger) lazy val secretNames = { val secretList = SecretList.list( ghRepoOrg, ghRepoName, token, backend, logger ).orExit(logger) secretList.secrets.map(_.name).toSet } val missingSetSecrets = missingFieldsWithDefaults .flatMap { case (_, default) => default.ghSecrets } .filter(s => s.force || !secretNames.contains(s.name)) if (missingSetSecrets.nonEmpty) { logger.message("") // printing an empty line, for readability logger.message { val name = if (missingSetSecrets.length <= 1) "secret" else "secrets" if (options.dummy) s"Checking ${missingSetSecrets.length} GitHub repository $name" else s"Uploading ${missingSetSecrets.length} GitHub repository $name" } LibSodiumJni.init(coursierCache, ArchiveCache().withCache(coursierCache), logger) lazy val pubKey = SecretCreate.publicKey( ghRepoOrg, ghRepoName, token, backend, logger ).orExit(logger) missingSetSecrets .map { s => if (options.dummy) { logger.message(s"Would have set GitHub secret ${s.name}") Right(true) } else SecretCreate.createOrUpdate( ghRepoOrg, ghRepoName, token, s.name, s.value, pubKey, dummy = false, printRequest = false, backend, logger ) } .sequence .left.map(CompositeBuildException(_)) .orExit(logger) } var written = Seq.empty[os.Path] if (missingFieldsWithDefaults.nonEmpty) { val missingFieldsWithDefaultsAndValues = missingFieldsWithDefaults .map { case (check, default) => default.getValue().map(v => (check, default, v)) } .sequence .left.map(CompositeBuildException(_)) .orExit(logger) val dest = { val ext = if (pureJava) ".java" else ".scala" inputs.workspace / s"publish-conf$ext" } val nl = System.lineSeparator() // FIXME Get from dest if it exists? def extraDirectivesLines(extraDirectives: Seq[(String, String)]) = extraDirectives.map { case (k, v) if v.exists(_.isWhitespace) => s"""//> using $k "$v"""" + nl case (k, v) => s"""//> using $k $v""" + nl }.mkString val extraLines = missingFieldsWithDefaultsAndValues.map { case (_, default, None) => extraDirectivesLines(default.extraDirectives) case (check, default, Some(value)) if value.exists(_.isWhitespace) => s"""//> using ${check.directivePath} "$value"""" + nl + extraDirectivesLines(default.extraDirectives) case (check, default, Some(value)) => s"""//> using ${check.directivePath} $value""" + nl + extraDirectivesLines(default.extraDirectives) } val currentContent = if (os.isFile(dest)) os.read.bytes(dest) else if (os.exists(dest)) sys.error(s"Error: $dest already exists and is not a file") else Array.emptyByteArray if (extraLines.nonEmpty) { val updatedContent = currentContent ++ extraLines.toArray.flatMap(_.getBytes(StandardCharsets.UTF_8)) os.write.over(dest, updatedContent) logger.message( s""" |Wrote ${CommandUtils.printablePath(dest)}""".stripMargin ) // printing an empty line, for readability written = written :+ dest } } if (options.checkWorkflow.getOrElse(options.publishParams.setupCi)) { val workflowDir = inputs.workspace / ".github" / "workflows" val hasWorkflows = os.isDir(workflowDir) && os.list(workflowDir) .filter(_.last.endsWith(".yml")) // FIXME Accept more extensions? .exists(os.isFile) if (hasWorkflows) logger.message( s"Found some workflow files under ${CommandUtils.printablePath(workflowDir)}, not writing Scala CLI workflow" ) else { val dest = workflowDir / "ci.yml" val content = { val resourcePath = Constants.defaultFilesResourcePath + "/workflows/default.yml" val cl = Thread.currentThread().getContextClassLoader val resUrl = cl.getResource(resourcePath) if (resUrl == null) sys.error(s"Should not happen - resource $resourcePath not found") val is = resUrl.openStream() try is.readAllBytes() finally is.close() } os.write(dest, content, createFolders = true) logger.message(s"Wrote workflow in ${CommandUtils.printablePath(dest)}") written = written :+ dest } } if (options.checkGitignore.getOrElse(true)) { val dest = inputs.workspace / ".gitignore" val hasGitignore = os.exists(dest) if (hasGitignore) logger.message( s"Found .gitignore under ${CommandUtils.printablePath(inputs.workspace)}, not writing one" ) else { val content = { val resourcePath = Constants.defaultFilesResourcePath + "/gitignore" val cl = Thread.currentThread().getContextClassLoader val resUrl = cl.getResource(resourcePath) if (resUrl == null) sys.error(s"Should not happen - resource $resourcePath not found") val is = resUrl.openStream() try is.readAllBytes() finally is.close() } os.write(dest, content, createFolders = true) logger.message(s"Wrote gitignore in ${CommandUtils.printablePath(dest)}") written = written :+ dest } } if (written.nonEmpty) logger.message("") // printing an empty line, for readability if (options.publishParams.setupCi && written.nonEmpty) logger.message( s"Commit and push ${written.map(CommandUtils.printablePath).mkString(", ")}, to enable publishing from CI" ) else logger.message("Project is ready for publishing!") if (!options.publishParams.setupCi) { logger.message("To publish your project, run") logger.message { val inputs = inputArgs .map(a => if (a.exists(_.isSpaceChar)) "\"" + a + "\"" else a) .mkString(" ") s" ${ScalaCli.progName} publish $inputs" } } } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/publish/PublishSetupOptions.scala ================================================ package scala.cli.commands.publish import caseapp.* import scala.cli.commands.pgp.{PgpScalaSigningOptions, SharedPgpPushPullOptions} import scala.cli.commands.shared.* import scala.cli.commands.tags import scala.cli.signing.shared.PasswordOption import scala.cli.signing.util.ArgParsers.* // format: off @HelpMessage(PublishSetupOptions.helpMessage, "", PublishSetupOptions.detailedHelpMessage) final case class PublishSetupOptions( @Recurse global: GlobalOptions = GlobalOptions(), @Recurse coursier: CoursierOptions = CoursierOptions(), @Recurse sharedVersionOptions: SharedVersionOptions = SharedVersionOptions(), @Recurse workspace: SharedWorkspaceOptions = SharedWorkspaceOptions(), @Recurse input: SharedInputOptions = SharedInputOptions(), @Recurse publishParams: PublishParamsOptions = PublishParamsOptions(), @Recurse publishRepo: PublishRepositoryOptions = PublishRepositoryOptions(), @Recurse sharedPgp: SharedPgpPushPullOptions = SharedPgpPushPullOptions(), @Recurse sharedJvm: SharedJvmOptions = SharedJvmOptions(), @Recurse scalaSigning: PgpScalaSigningOptions = PgpScalaSigningOptions(), @Group(HelpGroup.Publishing.toString) @Tag(tags.restricted) @Tag(tags.inShortHelp) @HelpMessage("Public key to use to verify artifacts (to be uploaded to a key server)") publicKey: Option[PasswordOption] = None, @Group(HelpGroup.Publishing.toString) @Tag(tags.restricted) @Tag(tags.inShortHelp) @HelpMessage("Check if some options for publishing are missing, and exit with non-zero return code if that's the case") check: Boolean = false, @Group(HelpGroup.Publishing.toString) @Tag(tags.restricted) @Tag(tags.inShortHelp) @HelpMessage("GitHub token to use to upload secrets to GitHub - password encoded") token: Option[PasswordOption] = None, @Group(HelpGroup.Publishing.toString) @Tag(tags.restricted) @Tag(tags.inShortHelp) @HelpMessage("Generate a random key pair for publishing, with a secret key protected by a random password") randomSecretKey: Option[Boolean] = None, @Group(HelpGroup.Publishing.toString) @Tag(tags.restricted) @Tag(tags.inShortHelp) @HelpMessage("When generating a random key pair, the mail to associate to it") randomSecretKeyMail: Option[String] = None, @Group(HelpGroup.Publishing.toString) @Tag(tags.restricted) @HelpMessage("The option groups to check - can be \"all\", or a comma-separated list of \"core\", \"signing\", \"repo\", \"extra\"") checks: Option[String] = None, @Group(HelpGroup.Publishing.toString) @Tag(tags.restricted) @HelpMessage("Whether to check if a GitHub workflow already exists (one for publishing is written if none is found)") checkWorkflow: Option[Boolean] = None, @Group(HelpGroup.Publishing.toString) @Tag(tags.restricted) @HelpMessage("Whether to check if a .gitignore file already exists (one is written if none is found)") checkGitignore: Option[Boolean] = None, @Group(HelpGroup.Publishing.toString) @Tag(tags.implementation) @HelpMessage("Dummy mode - don't upload any secret to GitHub") dummy: Boolean = false ) extends HasGlobalOptions // format: on object PublishSetupOptions { implicit lazy val parser: Parser[PublishSetupOptions] = Parser.derive implicit lazy val help: Help[PublishSetupOptions] = Help.derive val cmdName = "publish setup" private val helpHeader = "Configures the project for publishing." private val docWebsiteSuffix = "publishing/publish-setup" val helpMessage: String = s"""$helpHeader | |${HelpMessages.commandFullHelpReference(cmdName)} |${HelpMessages.commandDocWebsiteReference(docWebsiteSuffix)}""".stripMargin val detailedHelpMessage: String = s"""$helpHeader | |${HelpMessages.commandDocWebsiteReference(docWebsiteSuffix)}""".stripMargin } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/publish/PublishUtils.scala ================================================ package scala.cli.commands.publish import coursier.cache.{ArchiveCache, FileCache} import coursier.publish.signing.{GpgSigner, Signer} import java.net.URI import java.util.function.Supplier import scala.build.Ops.* import scala.build.errors.{BuildException, CompositeBuildException} import scala.build.options.{BuildOptions, ComputeVersion, PublishContextualOptions, PublishOptions} import scala.build.{Logger, ScalaArtifacts} import scala.cli.commands.pgp.PgpExternalCommand import scala.cli.commands.publish.ConfigUtil.* import scala.cli.config.{ConfigDb, Keys, PasswordOption, PublishCredentials} import scala.cli.errors.MissingPublishOptionError import scala.cli.publish.BouncycastleSignerMaker import scala.cli.util.ConfigPasswordOptionHelpers.* object PublishUtils { def getBouncyCastleSigner( secretKey: PasswordOption, secretKeyPasswordOpt: Option[PasswordOption], buildOptions: Option[BuildOptions], forceSigningExternally: Boolean, logger: Logger ): Signer = { val getLauncher: Supplier[Array[String]] = { () => val archiveCache = buildOptions.map(_.archiveCache) .getOrElse(ArchiveCache()) val fileCache = buildOptions.map(_.finalCache).getOrElse(FileCache()) PgpExternalCommand.launcher( fileCache, archiveCache, logger, buildOptions.getOrElse(BuildOptions()) ) match { case Left(e) => throw new Exception(e) case Right(binaryCommand) => binaryCommand.toArray } } (new BouncycastleSignerMaker).get( forceSigningExternally, secretKeyPasswordOpt.fold(null)(_.toCliSigning), secretKey.toCliSigning, getLauncher, logger ) } def getPublishCredentials( repo: String, configDb: () => ConfigDb ): Either[BuildException, Option[PublishCredentials]] = { val uri = new URI(repo) val isHttps = uri.getScheme == "https" val hostOpt = Option.when(isHttps)(uri.getHost) hostOpt match { case None => Right(None) case Some(host) => configDb().get(Keys.publishCredentials).wrapConfigException.map { credListOpt => credListOpt.flatMap { credList => credList.find { cred => cred.host == host && (isHttps || cred.httpsOnly.contains(false)) } } } } } extension (publishContextualOptions: PublishContextualOptions) { def getSecretKeyPasswordOpt(configDb: () => ConfigDb): Option[PasswordOption] = if publishContextualOptions.secretKeyPassword.isDefined then for { secretKeyPassConfigOpt <- publishContextualOptions.secretKeyPassword secretKeyPass <- secretKeyPassConfigOpt.get(configDb()).toOption } yield secretKeyPass else for { secretKeyPassOpt <- configDb().get(Keys.pgpSecretKeyPassword).toOption secretKeyPass <- secretKeyPassOpt } yield secretKeyPass def getGpgSigner: Either[MissingPublishOptionError, GpgSigner] = publishContextualOptions.gpgSignatureId.map { gpgSignatureId => GpgSigner( key = GpgSigner.Key.Id(gpgSignatureId), extraOptions = publishContextualOptions.gpgOptions ) }.toRight { new MissingPublishOptionError( name = "ID of the GPG key", optionName = "--gpgKey", directiveName = "" ) } } case class ArtifactData(org: String, name: String, version: String) extension (publishOptions: PublishOptions) { /** Maven POM `name` / ivy `m:name`: `publish.name` when set, otherwise the published artifact * name. */ def pomProjectNameForMaven(fallbackModuleName: String): String = publishOptions.name .map(_.value) .map(_.trim) .filter(_.nonEmpty) .getOrElse(fallbackModuleName) def artifactData( workspace: os.Path, logger: Logger, scalaArtifactsOpt: Option[ScalaArtifacts], isCi: Boolean ): Either[BuildException, ArtifactData] = { lazy val orgNameOpt = GitRepo.maybeGhRepoOrgName(workspace, logger) val maybeOrg = publishOptions.organization match { case Some(org0) => Right(org0.value) case None => orgNameOpt.map(_._1) match { case Some(org) => val mavenOrg = s"io.github.$org" logger.message( s"Using directive publish.organization not set, computed $mavenOrg from GitHub organization $org as default organization" ) Right(mavenOrg) case None => Left(new MissingPublishOptionError( "organization", "--organization", "publish.organization" )) } } val moduleName = publishOptions.moduleName match { case Some(name0) => name0.value case None => val name = publishOptions.name match { case Some(name0) => name0.value case None => val name = workspace.last logger.message( s"Using directive publish.name not specified, using workspace directory name $name as default name" ) name } scalaArtifactsOpt.map(_.params) match { case Some(scalaParams) => val pf = publishOptions.scalaPlatformSuffix.getOrElse { scalaParams.platform.fold("")("_" + _) } val sv = publishOptions.scalaVersionSuffix.getOrElse { // FIXME Allow full cross version too "_" + scalaParams.scalaBinaryVersion } name + pf + sv case None => name } } val maybeVer = publishOptions.version match { case Some(ver0) => Right(ver0.value) case None => val computeVer = publishOptions.contextual(isCi).computeVersion.orElse { def isGitRepo = GitRepo.gitRepoOpt(workspace).isDefined val default = ComputeVersion.defaultComputeVersion(!isCi && isGitRepo) if default.isDefined then logger.message( s"Using directive ${MissingPublishOptionError.versionError.directiveName} not set, assuming git:tag as publish.computeVersion" ) default } computeVer match { case Some(cv) => cv.get(workspace) case None => Left(MissingPublishOptionError.versionError) } } (maybeOrg, maybeVer) .traverseN .left.map(CompositeBuildException(_)) .map { case (org, ver) => ArtifactData(org, moduleName, ver) } } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/publish/RepoParams.scala ================================================ package scala.cli.commands.publish import coursier.core.Authentication import coursier.maven.MavenRepository import coursier.publish.sonatype.SonatypeApi import coursier.publish.util.EmaRetryParams import coursier.publish.{Hooks, PublishRepository} import java.net.URI import java.util.concurrent.ScheduledExecutorService import scala.build.EitherCps.{either, value} import scala.build.errors.BuildException import scala.build.internals.ConsoleUtils.ScalaCliConsole.warnPrefix import scala.build.{Logger, RepositoryUtils} import scala.cli.commands.util.ScalaCliSttpBackend final case class RepoParams( repo: PublishRepository, targetRepoOpt: Option[String], hooks: Hooks, isIvy2LocalLike: Boolean, defaultParallelUpload: Boolean, supportsSig: Boolean, acceptsChecksums: Boolean, shouldSign: Boolean, shouldAuthenticate: Boolean ) { import RepoParams.* def withAuth(auth: Authentication): RepoParams = copy( repo = repo.withAuthentication(auth), hooks = hooks match { case s: Hooks.Sonatype => s.copy( repo = s.repo.withAuthentication(auth), api = s.api.copy( authentication = Some(auth) ) ) case other => other } ) def withAuth(authOpt: Option[Authentication]): RepoParams = authOpt.fold(this)(withAuth) lazy val isSonatype: Boolean = Option(new URI(repo.snapshotRepo.root)) .filter(_.getScheme == "https") .map(_.getHost) .exists(sonatypeHosts.contains) } object RepoParams { private val sonatypeOssrhStagingApiBase = "https://ossrh-staging-api.central.sonatype.com" private val sonatypeSnapshotsBase = s"${RepositoryUtils.snapshotsRepositoryUrl}/" private val sonatypeLegacyBase = "https://oss.sonatype.org" private val sonatypeS01LegacyBase = "https://s01.oss.sonatype.org" private def sonatypeHosts: Seq[String] = Seq( sonatypeLegacyBase, sonatypeSnapshotsBase, sonatypeS01LegacyBase, sonatypeOssrhStagingApiBase ).map(new URI(_).getHost) def apply( repo: String, vcsUrlOpt: Option[String], workspace: os.Path, ivy2HomeOpt: Option[os.Path], isIvy2LocalLike: Boolean, es: ScheduledExecutorService, logger: Logger, connectionTimeoutRetries: Option[Int] = None, connectionTimeoutSeconds: Option[Int] = None, stagingRepoRetries: Option[Int] = None, stagingRepoWaitTimeMilis: Option[Int] = None ): Either[BuildException, RepoParams] = either { repo match { case "ivy2-local" => RepoParams.ivy2Local(ivy2HomeOpt) case "m2-local" | "maven-local" => RepoParams.m2Local(None) case "sonatype" | "central" | "maven-central" | "mvn-central" => logger.message(s"Using Portal OSSRH Staging API: $sonatypeOssrhStagingApiBase") RepoParams.centralRepo( base = sonatypeOssrhStagingApiBase, useLegacySnapshots = false, connectionTimeoutRetries = connectionTimeoutRetries, connectionTimeoutSeconds = connectionTimeoutSeconds, stagingRepoRetries = stagingRepoRetries, stagingRepoWaitTimeMilis = stagingRepoWaitTimeMilis, es = es, logger = logger ) case "sonatype-legacy" | "central-legacy" | "maven-central-legacy" | "mvn-central-legacy" => logger.message(s"$warnPrefix $sonatypeLegacyBase is EOL since 2025-06-30.") logger.message(s"$warnPrefix $sonatypeLegacyBase publishing is expected to fail.") RepoParams.centralRepo( base = sonatypeLegacyBase, useLegacySnapshots = true, connectionTimeoutRetries = connectionTimeoutRetries, connectionTimeoutSeconds = connectionTimeoutSeconds, stagingRepoRetries = stagingRepoRetries, stagingRepoWaitTimeMilis = stagingRepoWaitTimeMilis, es = es, logger = logger ) case "sonatype-s01" | "central-s01" | "maven-central-s01" | "mvn-central-s01" => logger.message(s"$warnPrefix $sonatypeS01LegacyBase is EOL since 2025-06-30.") logger.message(s"$warnPrefix it's expected publishing will fail.") RepoParams.centralRepo( base = sonatypeS01LegacyBase, useLegacySnapshots = true, connectionTimeoutRetries = connectionTimeoutRetries, connectionTimeoutSeconds = connectionTimeoutSeconds, stagingRepoRetries = stagingRepoRetries, stagingRepoWaitTimeMilis = stagingRepoWaitTimeMilis, es = es, logger = logger ) case "github" => value(RepoParams.gitHubRepo(vcsUrlOpt, workspace, logger)) case repoStr if repoStr.startsWith("github:") && repoStr.count(_ == '/') == 1 => val (org, name) = repoStr.stripPrefix("github:").split('/') match { case Array(org0, name0) => (org0, name0) case other => sys.error(s"Cannot happen ('$repoStr' -> ${other.toSeq})") } RepoParams.gitHubRepoFor(org, name) case repoStr => val repo0 = RepositoryParser.repositoryOpt(repoStr).getOrElse { val url = if (repoStr.contains("://")) repoStr else os.Path(repoStr, os.pwd).toNIO.toUri.toASCIIString MavenRepository(url) } RepoParams( repo = PublishRepository.Simple(repo0), targetRepoOpt = None, hooks = Hooks.dummy, isIvy2LocalLike = isIvy2LocalLike, defaultParallelUpload = true, supportsSig = true, acceptsChecksums = true, shouldSign = false, shouldAuthenticate = false ) } } def centralRepo( base: String, useLegacySnapshots: Boolean, connectionTimeoutRetries: Option[Int], connectionTimeoutSeconds: Option[Int], stagingRepoRetries: Option[Int], stagingRepoWaitTimeMilis: Option[Int], es: ScheduledExecutorService, logger: Logger ): RepoParams = { val repo0 = PublishRepository.Sonatype( base = MavenRepository(base), useLegacySnapshots = useLegacySnapshots ) val backend = ScalaCliSttpBackend.httpURLConnection(logger, connectionTimeoutSeconds) val api = SonatypeApi( backend = backend, base = base + "/service/local", authentication = None, verbosity = logger.verbosity, retryOnTimeout = connectionTimeoutRetries.getOrElse(3), stagingRepoRetryParams = EmaRetryParams( attempts = stagingRepoRetries.getOrElse(3), initialWaitDurationMs = stagingRepoWaitTimeMilis.getOrElse(10 * 1000), factor = 2.0f ) ) val hooks0 = Hooks.sonatype( repo = repo0, api = api, out = logger.compilerOutputStream, // meh verbosity = logger.verbosity, batch = coursier.paths.Util.useAnsiOutput(), // FIXME Get via logger es = es ) RepoParams( repo = repo0, targetRepoOpt = Some("https://repo1.maven.org/maven2"), hooks = hooks0, isIvy2LocalLike = false, defaultParallelUpload = true, supportsSig = true, acceptsChecksums = true, shouldSign = true, shouldAuthenticate = true ) } def gitHubRepoFor(org: String, name: String): RepoParams = RepoParams( repo = PublishRepository.Simple(MavenRepository(s"https://maven.pkg.github.com/$org/$name")), targetRepoOpt = None, hooks = Hooks.dummy, isIvy2LocalLike = false, defaultParallelUpload = false, supportsSig = false, acceptsChecksums = false, shouldSign = false, shouldAuthenticate = true ) def gitHubRepo( vcsUrlOpt: Option[String], workspace: os.Path, logger: Logger ): Either[BuildException, RepoParams] = either { val orgNameFromVcsOpt = vcsUrlOpt.flatMap(GitRepo.maybeGhOrgName) val (org, name) = orgNameFromVcsOpt match { case Some(orgName) => orgName case None => value(GitRepo.ghRepoOrgName(workspace, logger)) } gitHubRepoFor(org, name) } def ivy2Local(ivy2HomeOpt: Option[os.Path]): RepoParams = { val home = ivy2HomeOpt .orElse(sys.props.get("ivy.home").map(prop => os.Path(prop))) .orElse(sys.props.get("user.home").map(prop => os.Path(prop) / ".ivy2")) .getOrElse(os.home / ".ivy2") val base = home / "local" // not really a Maven repo… RepoParams( repo = PublishRepository.Simple(MavenRepository(base.toNIO.toUri.toASCIIString)), targetRepoOpt = None, hooks = Hooks.dummy, isIvy2LocalLike = true, defaultParallelUpload = true, supportsSig = true, acceptsChecksums = true, shouldSign = false, shouldAuthenticate = false ) } def m2Local(m2HomeOpt: Option[os.Path]): RepoParams = { val base = m2HomeOpt.getOrElse(os.home / ".m2" / "repository") RepoParams( repo = PublishRepository.Simple(MavenRepository(base.toNIO.toUri.toASCIIString)), targetRepoOpt = None, hooks = Hooks.dummy, isIvy2LocalLike = false, defaultParallelUpload = true, supportsSig = true, acceptsChecksums = true, shouldSign = false, shouldAuthenticate = false ) } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/publish/RepositoryParser.scala ================================================ package scala.cli.commands.publish // from coursier.internal.SharedRepositoryParser // delete when coursier.internal.SharedRepositoryParser.repositoryOpt is available for us import coursier.Repositories import coursier.maven.MavenRepository object RepositoryParser { def repositoryOpt(s: String): Option[MavenRepository] = if (s == "central") Some(Repositories.central) else if (s.startsWith("sonatype:")) Some(Repositories.sonatype(s.stripPrefix("sonatype:"))) else if (s.startsWith("bintray:")) { val s0 = s.stripPrefix("bintray:") val id = if (s.contains("/")) s0 else s0 + "/maven" Some(Repositories.bintray(id)) } else if (s.startsWith("typesafe:")) Some(Repositories.typesafe(s.stripPrefix("typesafe:"))) else if (s.startsWith("sbt-maven:")) Some(Repositories.sbtMaven(s.stripPrefix("sbt-maven:"))) else if (s == "scala-integration" || s == "scala-nightlies") Some(Repositories.scalaIntegration) else if (s == "jitpack") Some(Repositories.jitpack) else if (s == "clojars") Some(Repositories.clojars) else if (s == "jcenter") Some(Repositories.jcenter) else if (s == "google") Some(Repositories.google) else if (s == "gcs") Some(Repositories.centralGcs) else if (s == "gcs-eu") Some(Repositories.centralGcsEu) else if (s == "gcs-asia") Some(Repositories.centralGcsAsia) else if (s.startsWith("apache:")) Some(Repositories.apache(s.stripPrefix("apache:"))) else None } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/publish/SetSecret.scala ================================================ package scala.cli.commands.publish import scala.cli.config.Secret final case class SetSecret( name: String, value: Secret[String], force: Boolean ) ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/publish/SharedPublishOptions.scala ================================================ package scala.cli.commands.publish import caseapp.* import scala.build.compiler.{ScalaCompilerMaker, SimpleScalaCompilerMaker} import scala.cli.commands.shared.HelpGroup import scala.cli.commands.tags // format: off final case class SharedPublishOptions( @Group(HelpGroup.Publishing.toString) @HelpMessage("Directory where temporary files for publishing should be written") @Tag(tags.restricted) @Tag(tags.inShortHelp) @Hidden workingDir: Option[String] = None, @Group(HelpGroup.Publishing.toString) @Hidden @HelpMessage("Scala version suffix to append to the module name, like \"_2.13\" or \"_3\"") @ValueDescription("suffix") @Tag(tags.restricted) scalaVersionSuffix: Option[String] = None, @Group(HelpGroup.Publishing.toString) @Hidden @HelpMessage("Scala platform suffix to append to the module name, like \"_sjs1\" or \"_native0.4\"") @ValueDescription("suffix") @Tag(tags.restricted) scalaPlatformSuffix: Option[String] = None, @Group(HelpGroup.Publishing.toString) @HelpMessage("Whether to build and publish source JARs") @Name("sourcesJar") @Name("jarSources") @Name("sources") @Tag(tags.deprecated("sources")) @Tag(tags.restricted) @Tag(tags.inShortHelp) withSources: Option[Boolean] = None, @Group(HelpGroup.Publishing.toString) @HelpMessage("Whether to build and publish doc JARs") @ExtraName("scaladoc") @ExtraName("javadoc") @Tag(tags.restricted) @Tag(tags.inShortHelp) doc: Option[Boolean] = None, @Group(HelpGroup.Publishing.toString) @HelpMessage("ID of the GPG key to use to sign artifacts") @ValueDescription("key-id") @ExtraName("K") @Tag(tags.restricted) @Tag(tags.inShortHelp) gpgKey: Option[String] = None, @Group(HelpGroup.Publishing.toString) @HelpMessage("Method to use to sign artifacts") @ValueDescription("gpg|bc|none") @Tag(tags.restricted) @Tag(tags.inShortHelp) signer: Option[String] = None, @Group(HelpGroup.Publishing.toString) @HelpMessage("gpg command-line options") @ValueDescription("argument") @ExtraName("G") @ExtraName("gpgOpt") @Tag(tags.restricted) @Tag(tags.inShortHelp) gpgOption: List[String] = Nil, @Group(HelpGroup.Publishing.toString) @HelpMessage("Set Ivy 2 home directory") @ValueDescription("path") @Tag(tags.restricted) ivy2Home: Option[String] = None, @Group(HelpGroup.Publishing.toString) @Hidden @Tag(tags.restricted) checksum: List[String] = Nil, @Group(HelpGroup.Publishing.toString) @HelpMessage("Proceed as if publishing, but do not upload / write artifacts to the remote repository") @Tag(tags.implementation) dummy: Boolean = false ){ // format: on def docCompilerMakerOpt: Option[ScalaCompilerMaker] = if (doc.contains(false)) // true by default None else Some(SimpleScalaCompilerMaker("java", Nil, scaladoc = true)) } object SharedPublishOptions { implicit lazy val parser: Parser[SharedPublishOptions] = Parser.derive implicit lazy val help: Help[SharedPublishOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/publish/checks/CheckUtils.scala ================================================ package scala.cli.commands.publish.checks import java.net.URI import scala.build.Logger import scala.build.options.PublishOptions as BPublishOptions import scala.cli.commands.publish.{PublishSetupOptions, RepoParams} object CheckUtils { /** Keep in mind that combinedOptions do not contain all options from cliOptions, e.g. * publishRepo.publishRepository is not propagated */ def getRepoOpt( cliOptions: PublishSetupOptions, combinedOptions: BPublishOptions ): Option[String] = cliOptions.publishRepo.publishRepository .orElse { combinedOptions.contextual(cliOptions.publishParams.setupCi).repository } .orElse { if (cliOptions.publishParams.setupCi) combinedOptions.contextual(isCi = false).repository else None } def getHostOpt( options: PublishSetupOptions, pubOpt: BPublishOptions, workspace: os.Path, logger: Logger ): Option[String] = getRepoOpt(options, pubOpt).flatMap { repo => RepoParams( repo, pubOpt.versionControl.map(_.url), workspace, None, false, null, logger ) match { case Left(ex) => logger.debug("Caught exception when trying to compute host to check user credentials") logger.debug(ex) None case Right(params) => Some(new URI(params.repo.snapshotRepo.root).getHost) } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/publish/checks/ComputeVersionCheck.scala ================================================ package scala.cli.commands.publish.checks import scala.build.Logger import scala.build.errors.BuildException import scala.build.options.PublishOptions as BPublishOptions import scala.cli.commands.publish.{GitRepo, OptionCheck, PublishSetupOptions} import scala.cli.errors.MissingPublishOptionError final case class ComputeVersionCheck( options: PublishSetupOptions, workspace: os.Path, logger: Logger ) extends OptionCheck { def kind = OptionCheck.Kind.Core def fieldName = "computeVersion" def directivePath = "publish" + (if (options.publishParams.setupCi) ".ci" else "") + ".computeVersion" def check(pubOpt: BPublishOptions): Boolean = pubOpt.version.nonEmpty || pubOpt.retained(options.publishParams.setupCi).computeVersion.nonEmpty def defaultValue(pubOpt: BPublishOptions): Either[BuildException, OptionCheck.DefaultValue] = { def fromGitOpt = if (GitRepo.gitRepoOpt(workspace).isDefined) { logger.message("computeVersion:") logger.message(" assuming versions are computed from git tags") Some("git:tag") } else None val cv = options.sharedVersionOptions.computeVersion .orElse(fromGitOpt) cv.map(OptionCheck.DefaultValue.simple(_, Nil, Nil)).toRight { new MissingPublishOptionError( "compute version", "--compute-version", "publish.computeVersion" ) } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/publish/checks/DeveloperCheck.scala ================================================ package scala.cli.commands.publish.checks import scala.build.EitherCps.{either, value} import scala.build.Logger import scala.build.errors.BuildException import scala.build.options.PublishOptions as BPublishOptions import scala.cli.commands.publish.ConfigUtil.* import scala.cli.commands.publish.{OptionCheck, PublishSetupOptions} import scala.cli.config.{ConfigDb, Keys} import scala.cli.errors.MissingPublishOptionError final case class DeveloperCheck( options: PublishSetupOptions, configDb: () => ConfigDb, logger: Logger ) extends OptionCheck { def kind = OptionCheck.Kind.Extra def fieldName = "developers" def directivePath = "publish.developer" def check(pubOpt: BPublishOptions): Boolean = pubOpt.developers.nonEmpty def defaultValue(pubOpt: BPublishOptions): Either[BuildException, OptionCheck.DefaultValue] = either { // FIXME No headOption, add all of options.publishParams.developer values… val strValue = options.publishParams.developer.headOption match { case None => val nameOpt = value(configDb().get(Keys.userName).wrapConfigException) val emailOpt = value(configDb().get(Keys.userEmail).wrapConfigException) val urlOpt = value(configDb().get(Keys.userUrl).wrapConfigException) (nameOpt, emailOpt, urlOpt) match { case (Some(name), Some(email), Some(url)) => logger.message("developers:") logger.message(s" using $name <$email> ($url) from config") s"$name|$email|$url" case _ => value { Left { new MissingPublishOptionError( "developer", "--developer", "publish.developer", configKeys = Seq( Keys.userName.fullName, Keys.userEmail.fullName, Keys.userUrl.fullName ) ) } } } case Some(value) => value } OptionCheck.DefaultValue.simple(strValue, Nil, Nil) } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/publish/checks/LicenseCheck.scala ================================================ package scala.cli.commands.publish.checks import scala.build.Logger import scala.build.errors.BuildException import scala.build.options.PublishOptions as BPublishOptions import scala.cli.commands.publish.{OptionCheck, PublishSetupOptions} final case class LicenseCheck( options: PublishSetupOptions, logger: Logger ) extends OptionCheck { def kind = OptionCheck.Kind.Extra def fieldName = "license" def directivePath = "publish.license" def check(pubOpt: BPublishOptions): Boolean = pubOpt.license.nonEmpty private def defaultLicense = "Apache-2.0" def defaultValue(pubOpt: BPublishOptions): Either[BuildException, OptionCheck.DefaultValue] = { val license = options.publishParams.license.getOrElse { logger.message("license:") logger.message(s" using $defaultLicense (default)") defaultLicense } Right(OptionCheck.DefaultValue.simple(license, Nil, Nil)) } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/publish/checks/NameCheck.scala ================================================ package scala.cli.commands.publish.checks import scala.build.Logger import scala.build.errors.BuildException import scala.build.options.PublishOptions as BPublishOptions import scala.cli.commands.publish.{OptionCheck, PublishSetupOptions} final case class NameCheck( options: PublishSetupOptions, workspace: os.Path, logger: Logger ) extends OptionCheck { def kind = OptionCheck.Kind.Core def fieldName = "name" def directivePath = "publish.name" def check(options: BPublishOptions): Boolean = options.name.nonEmpty || options.moduleName.nonEmpty def defaultValue(pubOpt: BPublishOptions): Either[BuildException, OptionCheck.DefaultValue] = { def fromWorkspaceDirName = { val n = workspace.last logger.message("name:") logger.message(s" using workspace directory name $n") n } val name = options.publishParams.name.getOrElse(fromWorkspaceDirName) Right(OptionCheck.DefaultValue.simple(name, Nil, Nil)) } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/publish/checks/OrganizationCheck.scala ================================================ package scala.cli.commands.publish.checks import scala.build.Logger import scala.build.errors.BuildException import scala.build.options.PublishOptions as BPublishOptions import scala.cli.commands.publish.{GitRepo, OptionCheck, PublishSetupOptions} import scala.cli.errors.MissingPublishOptionError final case class OrganizationCheck( options: PublishSetupOptions, workspace: os.Path, logger: Logger ) extends OptionCheck { def kind = OptionCheck.Kind.Core def fieldName = "organization" def directivePath = "publish.organization" def check(options: BPublishOptions): Boolean = options.organization.nonEmpty def defaultValue(pubOpt: BPublishOptions): Either[BuildException, OptionCheck.DefaultValue] = { def viaGitHubRemoteOpt = GitRepo.ghRepoOrgName(workspace, logger) match { case Left(err) => logger.debug( s"Error when trying to get GitHub repo from git to compute default organization: $err, ignoring it." ) None case Right((org, _)) => val publishOrg = s"io.github.$org" logger.message("organization:") logger.message(s" computed $publishOrg from GitHub account $org") Some(publishOrg) } val orgOpt = options.publishParams.organization .orElse(viaGitHubRemoteOpt) orgOpt.map(OptionCheck.DefaultValue.simple(_, Nil, Nil)).toRight { new MissingPublishOptionError( "organization", "--organization", "publish.organization" ) } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/publish/checks/PasswordCheck.scala ================================================ package scala.cli.commands.publish.checks import scala.build.EitherCps.{either, value} import scala.build.Logger import scala.build.errors.BuildException import scala.build.options.PublishOptions as BPublishOptions import scala.cli.commands.publish.ConfigUtil.* import scala.cli.commands.publish.{OptionCheck, PublishSetupOptions, SetSecret} import scala.cli.config.{ConfigDb, Keys} import scala.cli.errors.MissingPublishOptionError final case class PasswordCheck( options: PublishSetupOptions, configDb: () => ConfigDb, workspace: os.Path, logger: Logger ) extends OptionCheck { def kind = OptionCheck.Kind.Repository def fieldName = "password" def directivePath = "publish" + (if (options.publishParams.setupCi) ".ci" else "") + ".password" private def passwordOpt(pubOpt: BPublishOptions) = CheckUtils.getHostOpt( options, pubOpt, workspace, logger ) match { case None => Right(None) case Some(host) => configDb().get(Keys.publishCredentials).wrapConfigException.map { credListOpt => credListOpt.flatMap { credList => credList .iterator .filter(_.host == host) .map(_.password) .collectFirst { case Some(p) => p } } } } def check(pubOpt: BPublishOptions): Boolean = pubOpt.retained(options.publishParams.setupCi).repoPassword.nonEmpty || !options.publishParams.setupCi && (passwordOpt(pubOpt) match { case Left(ex) => logger.debug("Ignoring error while trying to get password from config") logger.debug(ex) true case Right(valueOpt) => valueOpt.isDefined }) def defaultValue(pubOpt: BPublishOptions): Either[BuildException, OptionCheck.DefaultValue] = either { if (options.publishParams.setupCi) { val password = options.publishRepo.password match { case Some(password0) => password0.toConfig case None => value(passwordOpt(pubOpt)) match { case Some(password0) => logger.message("publish.credentials:") logger.message( s" using ${Keys.publishCredentials.fullName} from Scala CLI configuration" ) password0 case None => value { Left { new MissingPublishOptionError( "publish password", "--password", "publish.credentials", configKeys = Seq(Keys.publishCredentials.fullName) ) } } } } OptionCheck.DefaultValue.simple( "env:PUBLISH_PASSWORD", Nil, Seq(SetSecret("PUBLISH_PASSWORD", password.get(), force = true)) ) } else CheckUtils.getHostOpt( options, pubOpt, workspace, logger ) match { case None => logger.debug("No host, not checking for publish repository password") OptionCheck.DefaultValue.empty case Some(host) => if (value(passwordOpt(pubOpt)).isDefined) { logger.message("publish.password:") logger.message( s" found password for $host in ${Keys.publishCredentials.fullName} in Scala CLI configuration" ) OptionCheck.DefaultValue.empty } else { val optionName = CheckUtils.getRepoOpt(options, pubOpt).map(r => s"publish password for $r" ).getOrElse("publish password") value { Left { new MissingPublishOptionError( optionName, "", "publish.credentials", configKeys = Seq(Keys.publishCredentials.fullName) ) } } } } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/publish/checks/PgpSecretKeyCheck.scala ================================================ package scala.cli.commands.publish.checks import coursier.cache.{ArchiveCache, FileCache} import coursier.util.Task import sttp.client3.* import sttp.model.Uri import scala.build.EitherCps.{either, value} import scala.build.Logger import scala.build.Ops.* import scala.build.errors.{BuildException, CompositeBuildException, MalformedCliInputError} import scala.build.internal.util.WarningMessages import scala.build.options.PublishOptions as BPublishOptions import scala.build.options.publish.ConfigPasswordOption import scala.build.options.publish.ConfigPasswordOption.* import scala.cli.commands.config.ThrowawayPgpSecret import scala.cli.commands.pgp.{KeyServer, PgpProxyMaker} import scala.cli.commands.publish.ConfigUtil.* import scala.cli.commands.publish.{OptionCheck, PublishSetupOptions, SetSecret} import scala.cli.commands.util.JvmUtils import scala.cli.config.{ConfigDb, Keys} import scala.cli.errors.MissingPublishOptionError import scala.cli.signing.shared.PasswordOption import scala.cli.util.ConfigPasswordOptionHelpers.* /** Checks if: * - keys for signing files are present in using directives (either PGP or GPG) * - public key is uploaded to specified keyservers (if keys present are PGP) * * If any of the above fails then try the following to find missing keys: * - if secretKey using directive is already present then fill any missing from CLI options * - if secretKey is specified in options use only keys from options * - if --random-secret-key is specified and it's CI use new generated keys * - default to keys in config if this fails throw * * After previous step figures out which values should be used in setup do: * - if it's CI then upload github secrets and write using directives as 'using ci.key * env:GITHUB_SECRET_VAR' * - otherwise write down the key values to publish.conf file in the same form as they were * passed to options, if the values come from config don't write them to file * * Finally upload the public key to the keyservers that are specified */ final case class PgpSecretKeyCheck( options: PublishSetupOptions, coursierCache: FileCache[Task], configDb: () => ConfigDb, logger: Logger, backend: SttpBackend[Identity, Any] ) extends OptionCheck { def kind = OptionCheck.Kind.Signing def fieldName = "pgp-secret-key" def directivePath = "publish" + (if (options.publishParams.setupCi) ".ci" else "") + ".secretKey" def check(pubOpt: BPublishOptions): Boolean = { val opt0 = pubOpt.retained(options.publishParams.setupCi) opt0.repository.orElse(options.publishRepo.publishRepository).contains("github") || ( opt0.secretKey.isDefined && opt0.secretKeyPassword.isDefined && opt0.publicKey.isDefined && isKeyUploaded(opt0.publicKey.get.get(configDb()).toOption.map(_.toCliSigning)) .getOrElse(false) ) || opt0.gpgSignatureId.isDefined } def javaCommand: Either[BuildException, () => String] = either { () => value(JvmUtils.javaOptions(options.sharedJvm)).javaHome( ArchiveCache().withCache(coursierCache), coursierCache, logger.verbosity ).value.javaCommand } private lazy val keyServers: Either[BuildException, Seq[Uri]] = { val rawKeyServers = options.sharedPgp.keyServer.filter(_.trim.nonEmpty) if (rawKeyServers.isEmpty) Right(KeyServer.allDefaults) else rawKeyServers .map { keyServerUriStr => Uri.parse(keyServerUriStr).left.map { err => new MalformedCliInputError( s"Malformed key server URI '$keyServerUriStr': $err" ) } } .sequence .left.map(CompositeBuildException(_)) } /** Check if the public PGP key is uploaded to all keyservers that were specified */ private def isKeyUploaded(pubKeyOpt: Option[PasswordOption]): Either[BuildException, Boolean] = either { pubKeyOpt match { case Some(pubKey) => val keyId = value { (new PgpProxyMaker).get( options.scalaSigning.forceSigningExternally.getOrElse(false) ).keyId( pubKey.get().value, "[generated key]", coursierCache, logger, options.sharedJvm, options.coursier, options.scalaSigning.cliOptions() ) } value(keyServers).forall { keyServer => KeyServer.check(keyId, keyServer, backend) match case Right(Right(_)) => true case Right(Left(msg)) => logger.debug( s"""Response from $keyServer: |$msg |""".stripMargin ) false case Left(err) => logger.error(s"Error checking $keyId at $keyServer: $err") false } case None => false } } private case class PGPKeys( secretKeyOpt: Option[ConfigPasswordOption], secretKeyPasswordOpt: Option[ConfigPasswordOption], publicKeyOpt: Option[ConfigPasswordOption] ) val missingSecretKeyError = new MissingPublishOptionError( "publish secret key", "--secret-key", "publish.secretKey", configKeys = Seq(Keys.pgpSecretKey.fullName), extraMessage = "also specify publish.secretKeyPassword / --secret-key-password if needed." + (if (options.publishParams.setupCi) " Alternatively, pass --random-secret-key" else "") ) private lazy val keysFromOptions: PGPKeys = PGPKeys( options.publishParams.secretKey.map(_.configPasswordOptions()), options.publishParams.secretKeyPassword.map(_.configPasswordOptions()), options.publicKey.map(ConfigPasswordOption.ActualOption.apply) ) private lazy val maybeKeysFromConfig: Either[BuildException, PGPKeys] = for { secretKeyOpt <- configDb().get(Keys.pgpSecretKey).wrapConfigException pubKeyOpt <- configDb().get(Keys.pgpPublicKey).wrapConfigException passwordOpt <- configDb().get(Keys.pgpSecretKeyPassword).wrapConfigException } yield PGPKeys( secretKeyOpt.map(sk => ConfigPasswordOption.ActualOption(sk.toCliSigning)), passwordOpt.map(p => ConfigPasswordOption.ActualOption(p.toCliSigning)), pubKeyOpt.map(pk => ConfigPasswordOption.ActualOption(pk.toCliSigning)) ) private def getRandomPGPKeys: Either[BuildException, PGPKeys] = either { val maybeMail = options.randomSecretKeyMail.toRight( new MissingPublishOptionError( "the e-mail address to associate to the random key pair", "--random-secret-key-mail", "" ) ) val passwordSecret = options.publishParams.secretKeyPassword .map(_.configPasswordOptions()) .map { configPasswordOption => configPasswordOption .get(configDb()).wrapConfigException .map(_.get().toCliSigning) .orThrow } .getOrElse(ThrowawayPgpSecret.pgpPassPhrase()) val (pgpPublic, pgpSecret) = value { ThrowawayPgpSecret.pgpSecret( value(maybeMail), Some(passwordSecret), logger, coursierCache, options.sharedJvm, options.coursier, options.scalaSigning.cliOptions() ) } PGPKeys( Some(ConfigPasswordOption.ActualOption(PasswordOption.Value(pgpSecret))), Some(ConfigPasswordOption.ActualOption(PasswordOption.Value(passwordSecret))), Some(ConfigPasswordOption.ActualOption(PasswordOption.Value(pgpPublic))) ) } private def uploadKey(keyIdOpt: Option[ConfigPasswordOption]): Either[BuildException, Unit] = either { keyIdOpt match case None => logger.message( """ |Warning: no public key passed, not checking if the key needs to be uploaded to a key server.""".stripMargin ) // printing an empty line, for readability case Some(pubKeyConfigPasswordOption) => val publicKeyString = pubKeyConfigPasswordOption.get(configDb()) .orThrow .get() .value val keyId = (new PgpProxyMaker).get( options.scalaSigning.forceSigningExternally.getOrElse(false) ).keyId( publicKeyString, "[generated key]", coursierCache, logger, options.sharedJvm, options.coursier, options.scalaSigning.cliOptions() ).orThrow value(keyServers) .map { keyServer => if (options.dummy) { logger.message( s""" |Would upload key 0x${keyId.stripPrefix("0x")} to $keyServer""".stripMargin ) // printing an empty line, for readability Right(()) } else { val e: Either[BuildException, Unit] = either { val checkResp = value { KeyServer.check(keyId, keyServer, backend) .left.map(msg => new PgpSecretKeyCheck.KeyServerError( s"Error getting key $keyId from $keyServer: $msg" ) ) } logger.debug(s"Key server check response: $checkResp") val check = checkResp.isRight if (!check) { val resp = value { KeyServer.add(publicKeyString, keyServer, backend) .left.map(msg => new PgpSecretKeyCheck.KeyServerError( s"Error uploading key $keyId to $keyServer: $msg" ) ) } logger.debug(s"Key server upload response: $resp") logger.message( s""" |Uploaded key 0x${keyId.stripPrefix("0x")} to $keyServer""".stripMargin ) // printing an empty line, for readability } } e } } .sequence .left.map(CompositeBuildException(_)) .map(_ => ()) } def defaultValue(pubOpt: BPublishOptions): Either[BuildException, OptionCheck.DefaultValue] = either { val retainedOptions = pubOpt.retained(options.publishParams.setupCi) // obtain PGP keys that should be written to publish-conf file val (setupKeys, areConfigDefaults) = if (retainedOptions.secretKey.isDefined) { val publicKeySetup = if (retainedOptions.publicKey.isEmpty) keysFromOptions.publicKeyOpt else None val passwordSetup = if (retainedOptions.secretKeyPassword.isEmpty) keysFromOptions.secretKeyPasswordOpt else None (PGPKeys(None, passwordSetup, publicKeySetup), false) } else { val randomSecretKey = options.randomSecretKey.getOrElse(false) if (keysFromOptions.secretKeyOpt.isDefined) (keysFromOptions, false) else if ( // any PGP key option is specified, but there's no secretKey then notify the user keysFromOptions.publicKeyOpt.isDefined || keysFromOptions.secretKeyPasswordOpt.isDefined ) throw missingSecretKeyError else if (randomSecretKey && options.publishParams.setupCi) (getRandomPGPKeys.orThrow, false) else { val keysFromConfig = maybeKeysFromConfig.orThrow if (keysFromConfig.secretKeyOpt.isDefined) logger.message(s"$fieldName:") logger.message(" found keys in config") else throw missingSecretKeyError if (keysFromConfig.publicKeyOpt.isEmpty) logger.message(" warning: no PGP public key found in config") (keysFromConfig, true) } } val publicKeyOpt = retainedOptions.publicKey.orElse(setupKeys.publicKeyOpt) // if we setup for CI set GitHub secrets together with directives if (options.publishParams.setupCi) { val (passwordSetSecret, passwordDirectives) = setupKeys.secretKeyPasswordOpt .map { p => val dir = "publish.ci.secretKeyPassword" -> "env:PUBLISH_SECRET_KEY_PASSWORD" val secret = p.get(configDb()).orThrow.get() val setSec = SetSecret("PUBLISH_SECRET_KEY_PASSWORD", secret, force = true) (Seq(setSec), Seq(dir)) } .getOrElse((Nil, Nil)) val keySetSecrets = setupKeys.secretKeyOpt match case Some(configPasswordOption) => val secret = configPasswordOption.get(configDb()) .orThrow .get() Seq(SetSecret( "PUBLISH_SECRET_KEY", secret, force = true )) case _ => Nil val (publicKeySetSecret, publicKeyDirective) = setupKeys.publicKeyOpt .map { p => val dir = "publish.ci.publicKey" -> "env:PUBLISH_PUBLIC_KEY" val secret = p.get(configDb()).orThrow.get() val setSec = SetSecret("PUBLISH_PUBLIC_KEY", secret, force = true) (Seq(setSec), Seq(dir)) } .getOrElse((Nil, Nil)) val secretsToSet = keySetSecrets ++ passwordSetSecret ++ publicKeySetSecret val extraDirectives = passwordDirectives ++ publicKeyDirective OptionCheck.DefaultValue( () => uploadKey(publicKeyOpt).map(_ => Some("env:PUBLISH_SECRET_KEY")), extraDirectives, secretsToSet ) } else if (areConfigDefaults) OptionCheck.DefaultValue( () => uploadKey(publicKeyOpt).map(_ => None), Nil, Nil ) else { /** Obtain the text under the ConfigPasswordOption, e.g. "env:...", "file:...", "value:..." */ def getDirectiveValue(configPasswordOpt: Option[ConfigPasswordOption]): Option[String] = configPasswordOpt.collect { case ActualOption(passwordOption) => val optionValue = passwordOption.asString.value if (optionValue.startsWith("file:")) { val path = os.Path(optionValue.stripPrefix("file:")) scala.util.Try(path.relativeTo(os.pwd)) .map(p => s"file:${p.toString}") .getOrElse(optionValue) } else optionValue case ConfigOption(fullName) => s"config:$fullName" } val rawValueRegex = "^value:(.*)".r // Prevent potential leakage of a secret value val passwordDirectives = getDirectiveValue(setupKeys.secretKeyPasswordOpt) .flatMap { case rawValueRegex(rawValue) => logger.diagnostic( WarningMessages.rawValueNotWrittenToPublishFile( rawValue, "PGP password", "--secret-key-password" ) ) None case secretOption => Some("publish.secretKeyPassword" -> secretOption) } .toSeq // Prevent potential leakage of a secret value val secretKeyDirValue = getDirectiveValue(setupKeys.secretKeyOpt).flatMap { case rawValueRegex(rawValue) => logger.diagnostic( WarningMessages.rawValueNotWrittenToPublishFile( rawValue, "PGP secret key", "--secret-key" ) ) None case secretOption => Some(secretOption) } // This is safe to be publicly available val publicKeyDirective = getDirectiveValue(setupKeys.publicKeyOpt).map { "publish.publicKey" -> _ }.toSeq val extraDirectives = passwordDirectives ++ publicKeyDirective OptionCheck.DefaultValue( () => uploadKey(publicKeyOpt).map(_ => secretKeyDirValue), extraDirectives, Nil ) } } } object PgpSecretKeyCheck { final class KeyServerError(message: String) extends BuildException(message) } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/publish/checks/RepositoryCheck.scala ================================================ package scala.cli.commands.publish.checks import scala.build.Logger import scala.build.errors.BuildException import scala.build.options.PublishOptions as BPublishOptions import scala.cli.commands.publish.{OptionCheck, PublishSetupOptions} import scala.cli.errors.MissingPublishOptionError final case class RepositoryCheck( options: PublishSetupOptions, logger: Logger ) extends OptionCheck { def kind = OptionCheck.Kind.Repository def fieldName = "repository" def directivePath = "publish" + (if (options.publishParams.setupCi) ".ci" else "") + ".repository" def check(pubOpt: BPublishOptions): Boolean = pubOpt.retained(options.publishParams.setupCi).repository.nonEmpty def defaultValue(pubOpt: BPublishOptions): Either[BuildException, OptionCheck.DefaultValue] = { val maybeRepo = options.publishRepo.publishRepository .toRight(RepositoryCheck.missingValueError) .orElse { if (options.publishParams.setupCi) { val repoFromLocal = pubOpt.retained(isCi = false) .repository .toRight(RepositoryCheck.missingValueError) repoFromLocal.foreach { repoName => logger.message("repository:") logger.message(s" using repository from local configuration: $repoName") } repoFromLocal } else Left(RepositoryCheck.missingValueError) } maybeRepo.map(repo => OptionCheck.DefaultValue.simple(repo, Nil, Nil)) } } object RepositoryCheck { def missingValueError = new MissingPublishOptionError( "repository", "--publish-repository", "publish.repository", extraMessage = "use 'central' or 'central-s01' to publish to Maven Central via Sonatype." ) } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/publish/checks/ScmCheck.scala ================================================ package scala.cli.commands.publish.checks import scala.build.Logger import scala.build.errors.BuildException import scala.build.options.PublishOptions as BPublishOptions import scala.cli.commands.publish.{GitRepo, OptionCheck, PublishSetupOptions} import scala.cli.errors.MissingPublishOptionError final case class ScmCheck( options: PublishSetupOptions, workspace: os.Path, logger: Logger ) extends OptionCheck { def kind = OptionCheck.Kind.Extra def fieldName = "vcs" def directivePath = "publish.versionControl" def check(pubOpt: BPublishOptions): Boolean = pubOpt.versionControl.nonEmpty def defaultValue(pubOpt: BPublishOptions): Either[BuildException, OptionCheck.DefaultValue] = { def ghVcsOpt = GitRepo.ghRepoOrgName(workspace, logger) match { case Left(err) => logger.debug( s"Error when trying to get GitHub repo from git to get default project VCS: $err, ignoring it." ) None case Right((org, name)) => logger.message("vcs:") logger.message(s" using GitHub repository $org/$name") Some(s"github:$org/$name") } options.publishParams.vcs.orElse(ghVcsOpt).map( OptionCheck.DefaultValue.simple(_, Nil, Nil) ).toRight { new MissingPublishOptionError("version control", "--vcs", "publish.versionControl") } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/publish/checks/UrlCheck.scala ================================================ package scala.cli.commands.publish.checks import scala.build.Logger import scala.build.errors.BuildException import scala.build.options.PublishOptions as BPublishOptions import scala.cli.commands.publish.{GitRepo, OptionCheck, PublishSetupOptions} import scala.cli.errors.MissingPublishOptionError final case class UrlCheck( options: PublishSetupOptions, workspace: os.Path, logger: Logger ) extends OptionCheck { def kind = OptionCheck.Kind.Extra def fieldName = "url" def directivePath = "publish.url" def check(pubOpt: BPublishOptions): Boolean = pubOpt.url.nonEmpty def defaultValue(pubOpt: BPublishOptions): Either[BuildException, OptionCheck.DefaultValue] = { def ghUrlOpt = GitRepo.ghRepoOrgName(workspace, logger) match { case Left(err) => logger.debug( s"Error when trying to get GitHub repo from git to get default project URL: $err, ignoring it." ) None case Right((org, name)) => val url = s"https://github.com/$org/$name" logger.message("url:") logger.message(s" using GitHub repository URL $url") Some(url) } options.publishParams.url .orElse(ghUrlOpt) .map(OptionCheck.DefaultValue.simple(_, Nil, Nil)) .toRight { new MissingPublishOptionError("url", "--url", "publish.url") } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/publish/checks/UserCheck.scala ================================================ package scala.cli.commands.publish.checks import scala.build.EitherCps.{either, value} import scala.build.Logger import scala.build.errors.BuildException import scala.build.options.PublishOptions as BPublishOptions import scala.cli.commands.publish.ConfigUtil.* import scala.cli.commands.publish.{OptionCheck, PublishSetupOptions, SetSecret} import scala.cli.config.{ConfigDb, Keys} import scala.cli.errors.MissingPublishOptionError final case class UserCheck( options: PublishSetupOptions, configDb: () => ConfigDb, workspace: os.Path, logger: Logger ) extends OptionCheck { def kind = OptionCheck.Kind.Repository def fieldName = "user" def directivePath = "publish" + (if (options.publishParams.setupCi) ".ci" else "") + ".user" private def userOpt(pubOpt: BPublishOptions) = CheckUtils.getHostOpt( options, pubOpt, workspace, logger ) match { case None => Right(None) case Some(host) => configDb().get(Keys.publishCredentials).wrapConfigException.map { credListOpt => credListOpt.flatMap { credList => credList .iterator .filter(_.host == host) .map(_.user) .collectFirst { case Some(p) => p } } } } def check(pubOpt: BPublishOptions): Boolean = pubOpt.retained(options.publishParams.setupCi).repoUser.nonEmpty || !options.publishParams.setupCi && (userOpt(pubOpt) match { case Left(ex) => logger.debug("Ignoring error while trying to get user from config") logger.debug(ex) true case Right(valueOpt) => valueOpt.isDefined }) def defaultValue(pubOpt: BPublishOptions): Either[BuildException, OptionCheck.DefaultValue] = either { if (options.publishParams.setupCi) { val user0 = options.publishRepo.user match { case Some(value0) => value0.toConfig case None => value(userOpt(pubOpt)) match { case Some(user) => logger.message("publish.user:") logger.message( s" using ${Keys.publishCredentials.fullName} from Scala CLI configuration" ) user case None => value { Left { new MissingPublishOptionError( "publish user", "--user", "publish.credentials", configKeys = Seq(Keys.publishCredentials.fullName) ) } } } } OptionCheck.DefaultValue.simple( "env:PUBLISH_USER", Nil, Seq(SetSecret("PUBLISH_USER", user0.get(), force = true)) ) } else CheckUtils.getHostOpt( options, pubOpt, workspace, logger ) match { case None => logger.debug("No host, not checking for publish repository user") OptionCheck.DefaultValue.empty case Some(host) => if (value(userOpt(pubOpt).wrapConfigException).isDefined) { logger.message("publish.credentials:") logger.message( s" found user for $host in ${Keys.publishCredentials.fullName} in Scala CLI configuration" ) OptionCheck.DefaultValue.empty } else { val optionName = CheckUtils.getRepoOpt(options, pubOpt).map(r => s"publish user for $r").getOrElse( "publish user" ) value { Left { new MissingPublishOptionError( optionName, "", "publish.credentials", configKeys = Seq(Keys.publishCredentials.fullName) ) } } } } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala ================================================ package scala.cli.commands.repl import caseapp.* import caseapp.core.help.HelpFormat import coursier.cache.FileCache import coursier.error.ResolutionError import dependency.* import java.io.File import java.util.zip.ZipFile import scala.build.* import scala.build.EitherCps.{either, value} import scala.build.errors.{ BuildException, CantDownloadAmmoniteError, FetchingDependenciesError, MultipleScalaVersionsError } import scala.build.input.Inputs import scala.build.internal.{Constants, Runner} import scala.build.options.ScalacOpt.noDashPrefixes import scala.build.options.{BuildOptions, JavaOpt, MaybeScalaVersion, ScalaVersionUtil, Scope} import scala.cli.CurrentParams import scala.cli.commands.run.Run.{createPythonInstance, orPythonDetectionError, pythonPathEnv} import scala.cli.commands.run.RunMode import scala.cli.commands.shared.{HelpCommandGroup, HelpGroup, ScalacOptions, SharedOptions} import scala.cli.commands.util.BuildCommandHelpers import scala.cli.commands.{ScalaCommand, WatchUtil} import scala.cli.config.Keys import scala.cli.packaging.Library import scala.cli.util.ArgHelpers.* import scala.cli.util.ConfigDbUtils import scala.jdk.CollectionConverters.* import scala.util.Properties object Repl extends ScalaCommand[ReplOptions] with BuildCommandHelpers { override def group: String = HelpCommandGroup.Main.toString override def scalaSpecificationLevel = SpecificationLevel.MUST override def helpFormat: HelpFormat = super.helpFormat .withHiddenGroup(HelpGroup.Watch) .withPrimaryGroup(HelpGroup.Repl) override def names: List[List[String]] = List( List("repl"), List("console") ) override def sharedOptions(options: ReplOptions): Option[SharedOptions] = Some(options.shared) override def buildOptions(ops: ReplOptions): Some[BuildOptions] = Some(buildOptions0( ops, scala.cli.internal.Constants.maxAmmoniteScala3Version, scala.cli.internal.Constants.maxAmmoniteScala3LtsVersion )) private[commands] def buildOptions0( ops: ReplOptions, maxAmmoniteScalaVer: String, maxAmmoniteScalaLtsVer: String ): BuildOptions = { import ops.* import ops.sharedRepl.* val logger = ops.shared.logger val ammoniteVersionOpt = ammoniteVersion.map(_.trim).filter(_.nonEmpty) val baseOptions = shared.buildOptions(watchOptions = watch).orExit(logger) val maybeDowngradedScalaVersion = { val isDefaultAmmonite = ammonite.contains(true) && ammoniteVersionOpt.isEmpty extension (s: MaybeScalaVersion) private def isLtsAlias: Boolean = s.versionOpt.exists(v => ScalaVersionUtil.scala3Lts.contains(v.toLowerCase)) private def isLts: Boolean = s.versionOpt.exists(_.startsWith(Constants.scala3LtsPrefix)) || isLtsAlias baseOptions.scalaOptions.scalaVersion match { case Some(s) if isDefaultAmmonite && s.isLts && (s .versionOpt.exists(_.coursierVersion > maxAmmoniteScalaLtsVer.coursierVersion) || s.isLtsAlias) => val versionString = s.versionOpt.filter(_ => !s.isLtsAlias).getOrElse(Constants.scala3Lts) logger.message(s"Scala $versionString is not yet supported with this version of Ammonite") logger.message(s"Defaulting to Scala $maxAmmoniteScalaLtsVer") Some(MaybeScalaVersion(maxAmmoniteScalaLtsVer)) case None if isDefaultAmmonite && maxAmmoniteScalaVer.coursierVersion < defaultScalaVersion.coursierVersion => logger.message( s"Scala $defaultScalaVersion is not yet supported with this version of Ammonite" ) logger.message(s"Defaulting to Scala $maxAmmoniteScalaVer") Some(MaybeScalaVersion(maxAmmoniteScalaVer)) case s => s } } baseOptions.copy( scalaOptions = baseOptions.scalaOptions.copy(scalaVersion = maybeDowngradedScalaVersion), javaOptions = baseOptions.javaOptions.copy( javaOpts = baseOptions.javaOptions.javaOpts ++ sharedJava.allJavaOpts.map(JavaOpt(_)).map(Positioned.commandLine) ), notForBloopOptions = baseOptions.notForBloopOptions.copy( replOptions = baseOptions.notForBloopOptions.replOptions.copy( useAmmoniteOpt = ammonite, ammoniteVersionOpt = ammoniteVersion.map(_.trim).filter(_.nonEmpty), ammoniteArgs = ammoniteArg ), addRunnerDependencyOpt = baseOptions.notForBloopOptions.addRunnerDependencyOpt .orElse(Some(false)) ) ) } private def runMode(options: ReplOptions): RunMode.HasRepl = RunMode.Default override def runCommand(options: ReplOptions, args: RemainingArgs, logger: Logger): Unit = { val initialBuildOptions = buildOptionsOrExit(options) def default = Inputs.default().getOrElse { Inputs.empty(Os.pwd, options.shared.markdown.enableMarkdown) } val inputs = options.shared.inputs(args.remaining, defaultInputs = () => Some(default)).orExit(logger) val programArgs = args.unparsed CurrentParams.workspaceOpt = Some(inputs.workspace) val threads = BuildThreads.create() // compilerMaker should be a lazy val to prevent download a JAVA 17 for bloop when users run the repl without sources lazy val compilerMaker = options.shared.compilerMaker(threads) def doRunRepl( buildOptions: BuildOptions, allArtifacts: Seq[Artifacts], mainJarsOrClassDirs: Seq[os.Path], allowExit: Boolean, runMode: RunMode.HasRepl, successfulBuilds: Seq[Build.Successful] ): Unit = { val res = runRepl( options = buildOptions, programArgs = programArgs, allArtifacts = allArtifacts, mainJarsOrClassDirs = mainJarsOrClassDirs, logger = logger, allowExit = allowExit, dryRun = options.sharedRepl.replDryRun, runMode = runMode, successfulBuilds = successfulBuilds ) res match { case Left(ex) => if (allowExit) logger.exit(ex) else logger.log(ex) case Right(()) => } } def doRunReplFromBuild( builds: Seq[Build.Successful], allowExit: Boolean, runMode: RunMode.HasRepl, asJar: Boolean ): Unit = { doRunRepl( // build options should be the same for both scopes // combining them may cause for ammonite args to be duplicated, so we're using the main scope's opts buildOptions = builds.head.options, allArtifacts = builds.map(_.artifacts), mainJarsOrClassDirs = if asJar then Seq(Library.libraryJar(builds)) else builds.map(_.output), allowExit = allowExit, runMode = runMode, successfulBuilds = builds ) } val cross = options.sharedRepl.compileCross.cross.getOrElse(false) val configDb = ConfigDbUtils.configDb.orExit(logger) val actionableDiagnostics = options.shared.logging.verbosityOptions.actions.orElse( configDb.get(Keys.actions).getOrElse(None) ) val shouldBuildTestScope = options.shared.scope.test.getOrElse(false) if (inputs.isEmpty) { val allArtifacts = Seq(initialBuildOptions.artifacts(logger, Scope.Main).orExit(logger)) ++ (if shouldBuildTestScope then Seq(initialBuildOptions.artifacts(logger, Scope.Test).orExit(logger)) else Nil) // synchronizing, so that multiple presses to enter (handled by WatchUtil.waitForCtrlC) // don't try to run repls in parallel val lock = new Object def runThing() = lock.synchronized { doRunRepl( buildOptions = initialBuildOptions, allArtifacts = allArtifacts, mainJarsOrClassDirs = Seq.empty, allowExit = !options.sharedRepl.watch.watchMode, runMode = runMode(options), successfulBuilds = Seq.empty ) } runThing() if (options.sharedRepl.watch.watchMode) { // nothing to watch, just wait for Ctrl+C WatchUtil.printWatchMessage() WatchUtil.waitForCtrlC(() => runThing()) } } else if (options.sharedRepl.watch.watchMode) { val watcher = Build.watch( inputs, initialBuildOptions, compilerMaker, None, logger, crossBuilds = cross, buildTests = shouldBuildTestScope, partial = None, actionableDiagnostics = actionableDiagnostics, postAction = () => WatchUtil.printWatchMessage() ) { res => for (builds <- res.orReport(logger)) postBuild(builds, allowExit = false) { successfulBuilds => doRunReplFromBuild( successfulBuilds, allowExit = false, runMode = runMode(options), asJar = options.shared.asJar ) } } try WatchUtil.waitForCtrlC(() => watcher.schedule()) finally watcher.dispose() } else { val builds = Build.build( inputs, initialBuildOptions, compilerMaker, None, logger, crossBuilds = cross, buildTests = shouldBuildTestScope, partial = None, actionableDiagnostics = actionableDiagnostics ) .orExit(logger) postBuild(builds, allowExit = false) { successfulBuilds => doRunReplFromBuild( successfulBuilds, allowExit = true, runMode = runMode(options), asJar = options.shared.asJar ) } } } def postBuild(builds: Builds, allowExit: Boolean)(f: Seq[Build.Successful] => Unit): Unit = { if builds.anyBuildFailed then { System.err.println("Compilation failed") if allowExit then sys.exit(1) } else if builds.anyBuildCancelled then { System.err.println("Build cancelled") if allowExit then sys.exit(1) } else f(builds.builds.sortBy(_.scope).map(_.asInstanceOf[Build.Successful])) } private def maybeAdaptForWindows(args: Seq[String]): Seq[String] = if (Properties.isWin) args.map { a => if (a.contains(" ")) "\"" + a.replace("\"", "\\\"") + "\"" else a } else args private def runRepl( options: BuildOptions, programArgs: Seq[String], allArtifacts: Seq[Artifacts], mainJarsOrClassDirs: Seq[os.Path], logger: Logger, allowExit: Boolean, dryRun: Boolean, runMode: RunMode.HasRepl, successfulBuilds: Seq[Build.Successful] ): Either[BuildException, Unit] = either { val setupPython = options.notForBloopOptions.python.getOrElse(false) val cache = options.internal.cache.getOrElse(FileCache()) val shouldUseAmmonite = options.notForBloopOptions.replOptions.useAmmonite val scalaParams: ScalaParameters = value { val distinctScalaParams = allArtifacts.flatMap(_.scalaOpt).map(_.params).distinct if distinctScalaParams.isEmpty then Right(ScalaParameters(Constants.defaultScalaVersion)) else if distinctScalaParams.length == 1 then Right(distinctScalaParams.head) else Left(MultipleScalaVersionsError(distinctScalaParams.map(_.scalaVersion))) } val (scalapyJavaOpts, scalapyExtraEnv) = if (setupPython) { val props = value { val python = value(createPythonInstance().orPythonDetectionError) val propsOrError = python.scalapyProperties logger.debug(s"Python Java properties: $propsOrError") propsOrError.orPythonDetectionError } val props0 = props.toVector.sorted.map { case (k, v) => s"-D$k=$v" } // Putting current dir in PYTHONPATH, see // https://github.com/VirtusLab/scala-cli/pull/1616#issuecomment-1333283174 // for context. val dirs = successfulBuilds.map(_.inputs.workspace) ++ Seq(os.pwd) (props0, pythonPathEnv(dirs*)) } else (Nil, Map.empty[String, String]) def additionalArgs = { val pythonArgs = if (setupPython && scalaParams.scalaVersion.startsWith("2.13.")) Seq("-Yimports:java.lang,scala,scala.Predef,me.shadaj.scalapy") else Nil pythonArgs ++ options.scalaOptions.scalacOptions.toSeq.map(_.value.value) } def ammoniteAdditionalArgs() = { val pythonPredef = if (setupPython) """import me.shadaj.scalapy.py |import me.shadaj.scalapy.py.PyQuote |""".stripMargin else "" val predefArgs = if (pythonPredef.isEmpty) Nil else Seq("--predef-code", pythonPredef) predefArgs ++ options.notForBloopOptions.replOptions.ammoniteArgs } // TODO Warn if some entries of artifacts.classPath were evicted in replArtifacts.replClassPath // (should be artifacts whose version was bumped by Ammonite). // TODO Find the common namespace of all user classes, and import it all in the Ammonite session. // TODO Allow to disable printing the welcome banner and the "Loading..." message in Ammonite. val rootClasses = mainJarsOrClassDirs.flatMap { case dir if os.isDir(dir) => os.list(dir) .filter(_.last.endsWith(".class")) .filter(os.isFile(_)) // just in case .map(_.last.stripSuffix(".class")) .sorted case jar => var zf: ZipFile = null try { zf = new ZipFile(jar.toIO) zf.entries() .asScala .map(_.getName) .filter(!_.contains("/")) .filter(_.endsWith(".class")) .map(_.stripSuffix(".class")) .toVector .sorted } finally if (zf != null) zf.close() } val warnRootClasses = rootClasses.nonEmpty && options.notForBloopOptions.replOptions.useAmmoniteOpt.contains(true) if (warnRootClasses) logger.message( s"Warning: found classes defined in the root package (${rootClasses.mkString(", ")})." + " These will not be accessible from the REPL." ) def maybeRunRepl( replArtifacts: ReplArtifacts, replArgs: Seq[String], extraEnv: Map[String, String] = Map.empty, extraProps: Map[String, String] = Map.empty ): Unit = { val isAmmonite = replArtifacts.replMainClass.startsWith("ammonite") if isAmmonite then replArgs .map(_.noDashPrefixes) .filter(ScalacOptions.replExecuteScriptOptions.contains) .foreach(arg => logger.message( s"The '--$arg' option is not supported with Ammonite. Did you mean to use '--ammonite-arg -c' to execute a script?" ) ) if dryRun then logger.message("Dry run, not running REPL.") else { val depClassPathArgs: Seq[String] = if replArtifacts.depsClassPath.nonEmpty && !isAmmonite then Seq( "-classpath", (mainJarsOrClassDirs ++ replArtifacts.depsClassPath) .map(_.toString).mkString(File.pathSeparator) ) else Nil val replLauncherClasspath = if isAmmonite then mainJarsOrClassDirs ++ replArtifacts.replClassPath else replArtifacts.replClassPath val retCode = Runner.runJvm( javaCommand = options.javaHome().value.javaCommand, javaArgs = scalapyJavaOpts ++ replArtifacts.replJavaOpts ++ options.javaOptions.javaOpts.toSeq.map(_.value.value) ++ extraProps.toVector.sorted.map { case (k, v) => s"-D$k=$v" }, classPath = replLauncherClasspath, mainClass = replArtifacts.replMainClass, args = maybeAdaptForWindows(depClassPathArgs ++ replArgs), logger = logger, allowExecve = allowExit, extraEnv = scalapyExtraEnv ++ extraEnv ).waitFor() if (retCode != 0) value(Left(new ReplError(retCode))) } } def defaultArtifacts(): Either[BuildException, ReplArtifacts] = either { value { ReplArtifacts.default( scalaParams = scalaParams, dependencies = allArtifacts.flatMap(_.userDependencies).distinct, extraClassPath = allArtifacts.flatMap(_.extraClassPath).distinct, logger = logger, cache = cache, repositories = value(options.finalRepositories), addScalapy = if setupPython then Some(options.notForBloopOptions.scalaPyVersion.getOrElse(Constants.scalaPyVersion)) else None, javaVersion = options.javaHome().value.version ) } } def ammoniteArtifacts(): Either[BuildException, ReplArtifacts] = ReplArtifacts.ammonite( scalaParams = scalaParams, ammoniteVersion = options.notForBloopOptions.replOptions.ammoniteVersion(scalaParams.scalaVersion, logger), dependencies = allArtifacts.flatMap(_.userDependencies), extraClassPath = allArtifacts.flatMap(_.extraClassPath), extraSourceJars = allArtifacts.flatMap(_.extraSourceJars), extraRepositories = value(options.finalRepositories), logger = logger, cache = cache, addScalapy = if (setupPython) Some(options.notForBloopOptions.scalaPyVersion.getOrElse(Constants.scalaPyVersion)) else None ).left.map { case FetchingDependenciesError(e: ResolutionError.CantDownloadModule, positions) if shouldUseAmmonite && e.module.name.value == s"ammonite_${scalaParams.scalaVersion}" => CantDownloadAmmoniteError( e.versionConstraint.asString, scalaParams.scalaVersion, e, positions ) case other => other } if (shouldUseAmmonite) runMode match { case RunMode.Default => val replArtifacts = value(ammoniteArtifacts()) val replArgs = ammoniteAdditionalArgs() ++ programArgs maybeRunRepl(replArtifacts, replArgs) } else runMode match { case RunMode.Default => val replArtifacts = value(defaultArtifacts()) val replArgs = additionalArgs ++ programArgs maybeRunRepl(replArtifacts, replArgs) } } final class ReplError(retCode: Int) extends BuildException(s"Failed to run REPL (exit code: $retCode)") } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/repl/ReplOptions.scala ================================================ package scala.cli.commands.repl import caseapp.* import scala.cli.ScalaCli.fullRunnerName import scala.cli.commands.shared.{HasSharedOptions, HelpMessages, SharedOptions} @HelpMessage(ReplOptions.helpMessage, "", ReplOptions.detailedHelpMessage) // format: off final case class ReplOptions( @Recurse shared: SharedOptions = SharedOptions(), @Recurse sharedRepl: SharedReplOptions = SharedReplOptions() ) extends HasSharedOptions // format: on object ReplOptions { implicit lazy val parser: Parser[ReplOptions] = Parser.derive implicit lazy val help: Help[ReplOptions] = Help.derive val cmdName = "repl" private val helpHeader = "Fire-up a Scala REPL." val helpMessage: String = HelpMessages.shortHelpMessage(cmdName, helpHeader) val detailedHelpMessage: String = s"""$helpHeader | |The entire $fullRunnerName project's classpath is loaded to the repl. | |${HelpMessages.commandConfigurations(cmdName)} | |${HelpMessages.acceptedInputs} | |${HelpMessages.commandDocWebsiteReference(cmdName)}""".stripMargin } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/repl/SharedReplOptions.scala ================================================ package scala.cli.commands.repl import caseapp.* import caseapp.core.help.Help import scala.cli.commands.shared.{CrossOptions, HelpGroup, SharedJavaOptions, SharedWatchOptions} import scala.cli.commands.{Constants, tags} // format: off final case class SharedReplOptions( @Recurse sharedJava: SharedJavaOptions = SharedJavaOptions(), @Recurse watch: SharedWatchOptions = SharedWatchOptions(), @Recurse compileCross: CrossOptions = CrossOptions(), @Group(HelpGroup.Repl.toString) @Tag(tags.restricted) @Tag(tags.inShortHelp) @HelpMessage("Use Ammonite (instead of the default Scala REPL)") @Name("A") @Name("amm") ammonite: Option[Boolean] = None, @Group(HelpGroup.Repl.toString) @Tag(tags.restricted) @HelpMessage(s"Set the Ammonite version (${Constants.ammoniteVersion} by default)") @Name("ammoniteVer") @Tag(tags.inShortHelp) ammoniteVersion: Option[String] = None, @Group(HelpGroup.Repl.toString) @Name("a") @Tag(tags.restricted) @Tag(tags.inShortHelp) @HelpMessage("Provide arguments for ammonite repl") @Hidden ammoniteArg: List[String] = Nil, @Group(HelpGroup.Repl.toString) @Hidden @Tag(tags.implementation) @HelpMessage("Don't actually run the REPL, just fetch it") replDryRun: Boolean = false ) // format: on object SharedReplOptions { implicit lazy val parser: Parser[SharedReplOptions] = Parser.derive implicit lazy val help: Help[SharedReplOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/run/Run.scala ================================================ package scala.cli.commands.run import ai.kien.python.Python import caseapp.* import caseapp.core.help.HelpFormat import java.io.File import java.util.Locale import java.util.concurrent.CompletableFuture import java.util.concurrent.atomic.AtomicReference import scala.build.* import scala.build.EitherCps.{either, value} import scala.build.Ops.* import scala.build.errors.{BuildException, CompositeBuildException} import scala.build.input.* import scala.build.internal.{Constants, Runner, ScalaJsLinkerConfig} import scala.build.internals.ConsoleUtils.ScalaCliConsole import scala.build.internals.ConsoleUtils.ScalaCliConsole.warnPrefix import scala.build.internals.EnvVar import scala.build.options.{BuildOptions, JavaOpt, PackageType, Platform, Scope} import scala.cli.CurrentParams import scala.cli.commands.package0.Package import scala.cli.commands.setupide.SetupIde import scala.cli.commands.shared.{HelpCommandGroup, HelpGroup, SharedOptions} import scala.cli.commands.update.Update import scala.cli.commands.util.BuildCommandHelpers.* import scala.cli.commands.util.{BuildCommandHelpers, RunHadoop, RunSpark} import scala.cli.commands.{CommandUtils, ScalaCommand, SpecificationLevel, WatchUtil} import scala.cli.config.Keys import scala.cli.internal.ProcUtil import scala.cli.packaging.Library.fullClassPathMaybeAsJar import scala.cli.util.ArgHelpers.* import scala.cli.util.ConfigDbUtils import scala.util.{Properties, Try} object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { override def group: String = HelpCommandGroup.Main.toString override def scalaSpecificationLevel: SpecificationLevel = SpecificationLevel.MUST val primaryHelpGroups: Seq[HelpGroup] = Seq(HelpGroup.Run, HelpGroup.Entrypoint, HelpGroup.Watch) override def helpFormat: HelpFormat = super.helpFormat.withPrimaryGroups(primaryHelpGroups) override def sharedOptions(options: RunOptions): Option[SharedOptions] = Some(options.shared) private def runMode(options: RunOptions): RunMode = if options.sharedRun.standaloneSpark.getOrElse(false) && !options.sharedRun.sparkSubmit.contains(false) then RunMode.StandaloneSparkSubmit(options.sharedRun.submitArgument) else if options.sharedRun.sparkSubmit.getOrElse(false) then RunMode.SparkSubmit(options.sharedRun.submitArgument) else if options.sharedRun.hadoopJar then RunMode.HadoopJar else RunMode.Default private def scratchDirOpt(options: RunOptions): Option[os.Path] = options.sharedRun.scratchDir .filter(_.trim.nonEmpty) .map(os.Path(_, os.pwd)) override def runCommand(options: RunOptions, args: RemainingArgs, logger: Logger): Unit = runCommand( options0 = options, inputArgs = args.remaining, programArgs = args.unparsed, defaultInputs = () => Inputs.default(), logger = logger, invokeData = invokeData ) override def buildOptions(options: RunOptions): Some[BuildOptions] = Some { import options.* import options.sharedRun.* val logger = options.shared.logger val baseOptions = options.buildOptions().orExit(logger) baseOptions.copy( mainClass = mainClass.mainClass, javaOptions = baseOptions.javaOptions.copy( javaOpts = baseOptions.javaOptions.javaOpts ++ sharedJava.allJavaOpts.map(JavaOpt(_)).map(Positioned.commandLine), jvmIdOpt = baseOptions.javaOptions.jvmIdOpt.orElse { runMode(options) match { case _: RunMode.Spark | RunMode.HadoopJar => val sparkOrHadoopDefaultJvm = "8" logger.message( s"Defaulting the JVM to $sparkOrHadoopDefaultJvm for Spark/Hadoop runs." ) Some(Positioned.none(sparkOrHadoopDefaultJvm)) case RunMode.Default => None } } ), internal = baseOptions.internal.copy( keepResolution = baseOptions.internal.keepResolution || { runMode(options) match { case _: RunMode.Spark | RunMode.HadoopJar => true case RunMode.Default => false } } ), notForBloopOptions = baseOptions.notForBloopOptions.copy( runWithManifest = options.sharedRun.useManifest, addRunnerDependencyOpt = baseOptions.notForBloopOptions.addRunnerDependencyOpt.orElse { runMode(options) match { case _: RunMode.Spark | RunMode.HadoopJar => logger.debug(s"$warnPrefix Skipping the runner dependency when running Spark/Hadoop.") Some(false) case RunMode.Default => None } } ) ) } def runCommand( options0: RunOptions, inputArgs: Seq[String], programArgs: Seq[String], defaultInputs: () => Option[Inputs], logger: Logger, invokeData: ScalaCliInvokeData ): Unit = { val shouldDefaultServerFalse = inputArgs.isEmpty && options0.shared.compilationServer.server.isEmpty && !options0.shared.hasSnippets val options = if shouldDefaultServerFalse then { logger.debug("No inputs provided, skipping the build server.") options0.copy(shared = options0.shared.copy(compilationServer = options0.shared.compilationServer.copy(server = Some(false)) ) ) } else options0 val initialBuildOptions = { val buildOptions = buildOptionsOrExit(options) if invokeData.subCommand == SubCommand.Shebang then { val suppressDepUpdateOptions = buildOptions.suppressWarningOptions.copy( suppressOutdatedDependencyWarning = Some(true) ) buildOptions.copy( suppressWarningOptions = suppressDepUpdateOptions ) } else buildOptions } val inputs = options.shared.inputs(inputArgs, defaultInputs)(using invokeData).orExit(logger) CurrentParams.workspaceOpt = Some(inputs.workspace) val threads = BuildThreads.create() val compilerMaker = options.shared.compilerMaker(threads) def maybeRun( builds: Seq[Build.Successful], allowTerminate: Boolean, runMode: RunMode, showCommand: Boolean, scratchDirOpt: Option[os.Path] ): Either[BuildException, Seq[(Process, CompletableFuture[?])]] = either { val potentialMainClasses = builds.flatMap(_.foundMainClasses()).distinct if options.sharedRun.mainClass.mainClassLs.contains(true) then value { options.sharedRun.mainClass .maybePrintMainClasses(potentialMainClasses, shouldExit = allowTerminate) .map(_ => Seq.empty) } else { val processOrCommand: Either[Seq[Seq[String]], Seq[(Process, Option[() => Unit])]] = value { maybeRunOnce( builds = builds, args = programArgs, logger = logger, allowExecve = allowTerminate, jvmRunner = builds.exists(_.artifacts.hasJvmRunner), potentialMainClasses = potentialMainClasses, runMode = runMode, showCommand = showCommand, scratchDirOpt = scratchDirOpt, asJar = options.shared.asJar ) } processOrCommand match { case Right(processes) => processes.map { case (process, onExitOpt) => val onExitProcess = process.onExit().thenApply { p1 => val retCode = p1.exitValue() onExitOpt.foreach(_()) (retCode, allowTerminate) match { case (0, true) => case (0, false) => val gray = ScalaCliConsole.GRAY val reset = Console.RESET System.err.println(s"${gray}Program exited with return code $retCode.$reset") case (_, true) => sys.exit(retCode) case (_, false) => val red = Console.RED val lightRed = "\u001b[91m" val reset = Console.RESET System.err.println( s"${red}Program exited with return code $lightRed$retCode$red.$reset" ) } } (process, onExitProcess) } case Left(commands) => for { command <- commands arg <- command } println(arg) Seq.empty } } } val cross = options.sharedRun.compileCross.cross.getOrElse(false) if cross then logger.log( "Cross builds enabled, preparing all builds for all Scala versions and platforms..." ) SetupIde.runSafe( options = options.shared, inputs = inputs, logger = logger, buildOptions = initialBuildOptions, previousCommandName = Some(name), args = inputArgs ) if CommandUtils.shouldCheckUpdate then Update.checkUpdateSafe(logger) val configDb = ConfigDbUtils.configDb.orExit(logger) val actionableDiagnostics = options.shared.logging.verbosityOptions.actions.orElse( configDb.get(Keys.actions).getOrElse(None) ) val shouldBuildTestScope = options.shared.scope.test.getOrElse(false) if shouldBuildTestScope then logger.log("Test scope enabled, including test scope inputs on the classpath...") if options.sharedRun.watch.watchMode then { /** A handle to the Runner processes, used to kill the process if it's still alive when a * change occured and restarts are allowed or to wait for it if restarts are not allowed */ val processesRef = AtomicReference(Seq.empty[(Process, CompletableFuture[?])]) /** shouldReadInput controls whether [[WatchUtil.waitForCtrlC]](that's keeping the main thread * alive) should try to read StdIn or just call wait() */ val shouldReadInput = AtomicReference(false) /** A handle to the main thread to interrupt its operations when: * - it's blocked on reading StdIn, and it's no longer required * - it's waiting and should start reading StdIn */ val mainThreadOpt = AtomicReference(Option.empty[Thread]) val watcher = Build.watch( inputs = inputs, options = initialBuildOptions, compilerMaker = compilerMaker, docCompilerMakerOpt = None, logger = logger, crossBuilds = cross, buildTests = shouldBuildTestScope, partial = None, actionableDiagnostics = actionableDiagnostics, postAction = () => if processesRef.get().exists(_._1.isAlive()) then WatchUtil.printWatchWhileRunningMessage() else WatchUtil.printWatchMessage() ) { res => for ((process, onExitProcess) <- processesRef.get()) { onExitProcess.cancel(true) ProcUtil.interruptProcess(process, logger) } res.orReport(logger).map(_.builds).foreach { case b if b.forall(_.success) => val successfulBuilds = b.collect { case s: Build.Successful => s } for ((proc, _) <- processesRef.get() if proc.isAlive) // If the process doesn't exit, send SIGKILL ProcUtil.forceKillProcess(proc, logger) shouldReadInput.set(false) mainThreadOpt.get().foreach(_.interrupt()) val maybeProcesses = maybeRun( builds = successfulBuilds, allowTerminate = false, runMode = runMode(options), showCommand = options.sharedRun.command, scratchDirOpt = scratchDirOpt(options) ) .orReport(logger) .toSeq .flatten .map { case (proc, onExit) => if options.sharedRun.watch.restart then onExit.thenApply { _ => shouldReadInput.set(true) mainThreadOpt.get().foreach(_.interrupt()) } (proc, onExit) } successfulBuilds.foreach(_.copyOutput(options.shared)) if options.sharedRun.watch.restart then processesRef.set(maybeProcesses) else { for ((proc, onExit) <- maybeProcesses) ProcUtil.waitForProcess(proc, onExit) shouldReadInput.set(true) mainThreadOpt.get().foreach(_.interrupt()) } case b if b.exists(bb => !bb.success && !bb.cancelled) => System.err.println("Compilation failed") case _ => () } } mainThreadOpt.set(Some(Thread.currentThread())) try WatchUtil.waitForCtrlC( onPressEnter = { () => watcher.schedule() shouldReadInput.set(false) }, shouldReadInput = () => shouldReadInput.get() ) finally { mainThreadOpt.set(None) watcher.dispose() } } else Build.build( inputs = inputs, options = initialBuildOptions, compilerMaker = compilerMaker, docCompilerMakerOpt = None, logger = logger, crossBuilds = cross, buildTests = shouldBuildTestScope, partial = None, actionableDiagnostics = actionableDiagnostics ) .orExit(logger) .all match { case b if b.forall(_.success) => val successfulBuilds = b.collect { case s: Build.Successful => s } successfulBuilds.foreach(_.copyOutput(options.shared)) val results = maybeRun( builds = successfulBuilds, allowTerminate = true, runMode = runMode(options), showCommand = options.sharedRun.command, scratchDirOpt = scratchDirOpt(options) ) .orExit(logger) for ((process, onExit) <- results) ProcUtil.waitForProcess(process, onExit) case b if b.exists(bb => !bb.success && !bb.cancelled) => System.err.println("Compilation failed") sys.exit(1) case _ => () } } private def maybeRunOnce( builds: Seq[Build.Successful], args: Seq[String], logger: Logger, allowExecve: Boolean, jvmRunner: Boolean, potentialMainClasses: Seq[String], runMode: RunMode, showCommand: Boolean, scratchDirOpt: Option[os.Path], asJar: Boolean ): Either[BuildException, Either[Seq[Seq[String]], Seq[(Process, Option[() => Unit])]]] = either { val mainClassOpt = builds.head.options.mainClass.filter(_.nonEmpty) // trim it too? .orElse { if builds.head.options.jmhOptions.enableJmh .contains(true) && !builds.head.options.jmhOptions.canRunJmh then Some("org.openjdk.jmh.Main") else None } val mainClass: String = mainClassOpt match { case Some(cls) => cls case None => val retainedMainClassesByScope: Map[Scope, String] = value { builds .map { build => build.retainedMainClass(logger, mainClasses = potentialMainClasses) .map(mainClass => build.scope -> mainClass) } .sequence .left .map(CompositeBuildException(_)) .map(_.toMap) } if retainedMainClassesByScope.size == 1 then retainedMainClassesByScope.head._2 else retainedMainClassesByScope .get(Scope.Main) .orElse(retainedMainClassesByScope.get(Scope.Test)) .get } logger.debug(s"Retained main class: $mainClass") val verbosity = builds.head.options.internal.verbosity.getOrElse(0).toString val (finalMainClass, finalArgs) = if jvmRunner then (Constants.runnerMainClass, mainClass +: verbosity +: args) else (mainClass, args) logger.debug(s"Final main class: $finalMainClass") val res = runOnce( allBuilds = builds, mainClass = finalMainClass, args = finalArgs, logger = logger, allowExecve = allowExecve, runMode = runMode, showCommand = showCommand, scratchDirOpt = scratchDirOpt, asJar = asJar ) value(res) } def pythonPathEnv(dirs: os.Path*): Map[String, String] = { val onlySafePaths = sys.env.exists { case (k, v) => k.toLowerCase(Locale.ROOT) == "pythonsafepath" && v.nonEmpty } // Don't add unsafe directories to PYTHONPATH if PYTHONSAFEPATH is set, // see https://docs.python.org/3/using/cmdline.html#envvar-PYTHONSAFEPATH // and https://github.com/VirtusLab/scala-cli/pull/1616#issuecomment-1336017760 // for more details. if onlySafePaths then Map.empty[String, String] else { val (pythonPathEnvVarName, currentPythonPath) = sys.env .find(_._1.toLowerCase(Locale.ROOT) == "pythonpath") .getOrElse(("PYTHONPATH", "")) val updatedPythonPath = (currentPythonPath +: dirs.map(_.toString)) .filter(_.nonEmpty) .mkString(File.pathSeparator) Map(pythonPathEnvVarName -> updatedPythonPath) } } private def runOnce( allBuilds: Seq[Build.Successful], mainClass: String, args: Seq[String], logger: Logger, allowExecve: Boolean, runMode: RunMode, showCommand: Boolean, scratchDirOpt: Option[os.Path], asJar: Boolean ): Either[BuildException, Either[Seq[Seq[String]], Seq[(Process, Option[() => Unit])]]] = { val crossBuilds = allBuilds.groupedByCrossParams.toSeq val shouldLogCrossInfo = crossBuilds.size > 1 // execve replaces the current process, so we must not use it when spawning multiple cross-builds val effectiveAllowExecve = allowExecve && !shouldLogCrossInfo if shouldLogCrossInfo then logger.log( s"Running ${crossBuilds.size} cross builds, one for each Scala version and platform combination." ) crossBuilds .map { (crossBuildParams, builds) => if shouldLogCrossInfo then logger.debug(s"Running build for ${crossBuildParams.asString}") val build = builds.head either { build.options.platform.value match { case Platform.JS => val esModule = build.options.scalaJsOptions.moduleKindStr.exists(m => m == "es" || m == "esmodule") val linkerConfig = builds.head.options.scalaJsOptions.linkerConfig(logger) val jsDest = { val delete = scratchDirOpt.isEmpty scratchDirOpt.foreach(os.makeDir.all(_)) os.temp( dir = scratchDirOpt.orNull, prefix = "main", suffix = if esModule then ".mjs" else ".js", deleteOnExit = delete ) } val res = Package.linkJs( builds = builds, dest = jsDest, mainClassOpt = Some(mainClass), addTestInitializer = false, config = linkerConfig, fullOpt = value(build.options.scalaJsOptions.fullOpt), noOpt = build.options.scalaJsOptions.noOpt.getOrElse(false), logger = logger, scratchDirOpt = scratchDirOpt ).map { outputPath => val jsDom = build.options.scalaJsOptions.dom.getOrElse(false) if showCommand then Left(Runner.jsCommand(outputPath.toIO, args, jsDom = jsDom)) else { val process = value { Runner.runJs( outputPath.toIO, args, logger, allowExecve = effectiveAllowExecve, jsDom = jsDom, sourceMap = build.options.scalaJsOptions.emitSourceMaps, esModule = esModule ) } process.onExit().thenApply(_ => if os.exists(jsDest) then os.remove(jsDest)) Right((process, None)) } } value(res) case Platform.Native => val setupPython = build.options.notForBloopOptions.doSetupPython.getOrElse(false) val (pythonExecutable, pythonLibraryPaths, pythonExtraEnv) = if setupPython then { val (exec, libPaths) = value { val python = value(createPythonInstance().orPythonDetectionError) val pythonPropertiesOrError = for { paths <- python.nativeLibraryPaths executable <- python.executable } yield (Some(executable), paths) logger.debug( s"Python executable and native library paths: $pythonPropertiesOrError" ) pythonPropertiesOrError.orPythonDetectionError } // Putting the workspace in PYTHONPATH, see // https://github.com/VirtusLab/scala-cli/pull/1616#issuecomment-1333283174 // for context. (exec, libPaths, pythonPathEnv(builds.head.inputs.workspace)) } else (None, Nil, Map()) // seems conda doesn't add the lib directory to LD_LIBRARY_PATH (see conda/conda#308), // which prevents apps from finding libpython for example, so we update it manually here val libraryPathsEnv = if pythonLibraryPaths.isEmpty then Map.empty else { val prependTo = if Properties.isWin then EnvVar.Misc.path.name else if Properties.isMac then EnvVar.Misc.dyldLibraryPath.name else EnvVar.Misc.ldLibraryPath.name val currentOpt = Option(System.getenv(prependTo)) val currentEntries = currentOpt .map(_.split(File.pathSeparator).toSet) .getOrElse(Set.empty) val additionalEntries = pythonLibraryPaths.filter(!currentEntries.contains(_)) if additionalEntries.isEmpty then Map.empty else { val newValue = (additionalEntries.iterator ++ currentOpt.iterator).mkString( File.pathSeparator ) Map(prependTo -> newValue) } } val programNameEnv = pythonExecutable.fold(Map.empty)(py => Map("SCALAPY_PYTHON_PROGRAMNAME" -> py)) val extraEnv = libraryPathsEnv ++ programNameEnv ++ pythonExtraEnv val maybeResult = withNativeLauncher( builds, mainClass, logger ) { launcher => if showCommand then Left( extraEnv.toVector.sorted.map { case (k, v) => s"$k=$v" } ++ Seq(launcher.toString) ++ args ) else { val proc = Runner.runNative( launcher = launcher.toIO, args = args, logger = logger, allowExecve = effectiveAllowExecve, extraEnv = extraEnv ) Right((proc, None)) } } value(maybeResult) case Platform.JVM => def fwd(s: String): String = s.replace('\\', '/') def base(s: String): String = fwd(s).replaceAll(".*/", "") runMode match { case RunMode.Default => val sourceFiles = builds.head.inputs.sourceFiles().map { case s: ScalaFile => fwd(s.path.toString) case s: Script => fwd(s.path.toString) case s: MarkdownFile => fwd(s.path.toString) case _: SbtFile => "" case s: OnDisk => fwd(s.path.toString) case s => s.getClass.getName }.filter(_.nonEmpty).distinct val sources = sourceFiles.mkString(File.pathSeparator) val sourceNames = sourceFiles.map(base).mkString(File.pathSeparator) val baseJavaProps = build.options.javaOptions.javaOpts.toSeq.map(_.value.value) ++ Seq(s"-Dscala.sources=$sources", s"-Dscala.source.names=$sourceNames") val setupPython = build.options.notForBloopOptions.doSetupPython.getOrElse(false) val (pythonJavaProps, pythonExtraEnv) = if setupPython then { val scalapyProps = value { val python = value(createPythonInstance().orPythonDetectionError) val propsOrError = python.scalapyProperties logger.debug(s"Python Java properties: $propsOrError") propsOrError.orPythonDetectionError } val props = scalapyProps.toVector.sorted.map { case (k, v) => s"-D$k=$v" } // Putting the workspace in PYTHONPATH, see // https://github.com/VirtusLab/scala-cli/pull/1616#issuecomment-1333283174 // for context. (props, pythonPathEnv(build.inputs.workspace)) } else (Nil, Map.empty[String, String]) val allJavaOpts = pythonJavaProps ++ baseJavaProps if showCommand then Left { Runner.jvmCommand( build.options.javaHome().value.javaCommand, allJavaOpts, builds.flatMap(_.fullClassPathMaybeAsJar(asJar)).distinct, mainClass, args, extraEnv = pythonExtraEnv, useManifest = build.options.notForBloopOptions.runWithManifest, scratchDirOpt = scratchDirOpt ) } else { val proc = Runner.runJvm( javaCommand = build.options.javaHome().value.javaCommand, javaArgs = allJavaOpts, classPath = builds.flatMap(_.fullClassPathMaybeAsJar(asJar)).distinct, mainClass = mainClass, args = args, logger = logger, allowExecve = effectiveAllowExecve, extraEnv = pythonExtraEnv, useManifest = build.options.notForBloopOptions.runWithManifest, scratchDirOpt = scratchDirOpt ) Right((proc, None)) } case mode: RunMode.SparkSubmit => value { RunSpark.run( builds = builds, mainClass = mainClass, args = args, submitArgs = mode.submitArgs, logger = logger, allowExecve = effectiveAllowExecve, showCommand = showCommand, scratchDirOpt = scratchDirOpt ) } case mode: RunMode.StandaloneSparkSubmit => value { RunSpark.runStandalone( builds = builds, mainClass = mainClass, args = args, submitArgs = mode.submitArgs, logger = logger, allowExecve = effectiveAllowExecve, showCommand = showCommand, scratchDirOpt = scratchDirOpt ) } case RunMode.HadoopJar => value { RunHadoop.run( builds = builds, mainClass = mainClass, args = args, logger = logger, allowExecve = effectiveAllowExecve, showCommand = showCommand, scratchDirOpt = scratchDirOpt ) } } } } } .sequence .left.map(CompositeBuildException(_)) .map(_.sequence.left.map(_.toSeq)) } def withLinkedJs[T]( builds: Seq[Build.Successful], mainClassOpt: Option[String], addTestInitializer: Boolean, config: ScalaJsLinkerConfig, fullOpt: Boolean, noOpt: Boolean, logger: Logger, esModule: Boolean )(f: os.Path => T): Either[BuildException, T] = { val dest = os.temp(prefix = "main", suffix = if esModule then ".mjs" else ".js") try Package.linkJs( builds = builds, dest = dest, mainClassOpt = mainClassOpt, addTestInitializer = addTestInitializer, config = config, fullOpt = fullOpt, noOpt = noOpt, logger = logger ).map(outputPath => f(outputPath)) finally if os.exists(dest) then os.remove(dest) } def withNativeLauncher[T]( builds: Seq[Build.Successful], mainClass: String, logger: Logger )(f: os.Path => T): Either[BuildException, T] = Package.buildNative( builds = builds, mainClass = Some(mainClass), targetType = PackageType.Native.Application, destPath = None, logger = logger ).map(f) def findHomebrewPython(): Option[os.Path] = if (Properties.isMac) { // Try common Homebrew locations val homebrewPaths = Seq( "/opt/homebrew/bin/python3", "/usr/local/bin/python3" ) homebrewPaths .map(os.Path(_, os.pwd)) .find { path => os.exists(path) && { // Verify it has the -config script val configPath = path / os.up / s"${path.last}-config" os.exists(configPath) } } } else None def createPythonInstance(): Try[Python] = // Try default Python detection first // If it fails on macOS and Homebrew Python is available, the error message will guide the user Try(Python()) final class PythonDetectionError(cause: Throwable) extends BuildException( { val baseMessage = cause.getMessage if ( Properties.isMac && baseMessage != null && baseMessage.contains( "-config" ) && baseMessage.contains("does not exist") ) { val homebrewPython = findHomebrewPython() val homebrewHint = homebrewPython match { case Some(path) => s""" |Homebrew Python was found at $path, but it's not being used. |Ensure Homebrew's bin directory is first in your PATH: | export PATH="${path / os.up}:$$PATH" |""".stripMargin case None => """ |Consider installing Python via Homebrew: | brew install python | |Or install Python from https://www.python.org/downloads/ |""".stripMargin } s"""Error detecting Python environment: $baseMessage | |The system Python from CommandLineTools may not include the required -config scripts. |$homebrewHint | |Alternatively, you can disable Python setup with --python=false""".stripMargin } else s"Error detecting Python environment: $baseMessage" }, cause = cause ) extension [T](t: Try[T]) def orPythonDetectionError: Either[PythonDetectionError, T] = t.toEither.left.map(new PythonDetectionError(_)) } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/run/RunMode.scala ================================================ package scala.cli.commands.run sealed abstract class RunMode extends Product with Serializable object RunMode { sealed abstract class HasRepl extends RunMode sealed abstract class Spark extends RunMode { def submitArgs: Seq[String] def withSubmitArgs(args: Seq[String]): Spark } case object Default extends HasRepl final case class SparkSubmit(submitArgs: Seq[String]) extends Spark { def withSubmitArgs(args: Seq[String]): SparkSubmit = copy(submitArgs = args) } final case class StandaloneSparkSubmit(submitArgs: Seq[String]) extends Spark { def withSubmitArgs(args: Seq[String]): StandaloneSparkSubmit = copy(submitArgs = args) } case object HadoopJar extends RunMode } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/run/RunOptions.scala ================================================ package scala.cli.commands.run import caseapp.* import caseapp.core.help.Help import scala.cli.ScalaCli import scala.cli.commands.shared.{ HasSharedOptions, HasSharedWatchOptions, HelpMessages, SharedOptions, SharedWatchOptions } @HelpMessage(RunOptions.helpMessage, "", RunOptions.detailedHelpMessage) // format: off final case class RunOptions( @Recurse shared: SharedOptions = SharedOptions(), @Recurse sharedRun: SharedRunOptions = SharedRunOptions() ) extends HasSharedOptions with HasSharedWatchOptions { // format: on override def watch: SharedWatchOptions = sharedRun.watch } object RunOptions { implicit lazy val parser: Parser[RunOptions] = Parser.derive implicit lazy val help: Help[RunOptions] = Help.derive val cmdName = "run" private val helpHeader = "Compile and run Scala code." val helpMessage: String = HelpMessages.shortHelpMessage(cmdName, helpHeader) val detailedHelpMessage: String = s"""$helpHeader | |${HelpMessages.commandConfigurations(cmdName)} | |For a run to be successful, a main method must be present on the classpath. |.sc scripts are an exception, as a main class is provided in their wrapper. | |${HelpMessages.acceptedInputs} | |To pass arguments to the actual application, just add them after `--`, like: | ${Console.BOLD}${ScalaCli .progName} run Main.scala AnotherSource.scala -- first-arg second-arg${Console.RESET} | |${HelpMessages.commandDocWebsiteReference(cmdName)}""".stripMargin } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/run/SharedRunOptions.scala ================================================ package scala.cli.commands.run import caseapp.* import caseapp.core.help.Help import scala.cli.commands.shared.* import scala.cli.commands.tags // format: off final case class SharedRunOptions( @Recurse sharedJava: SharedJavaOptions = SharedJavaOptions(), @Recurse watch: SharedWatchOptions = SharedWatchOptions(), @Recurse compileCross: CrossOptions = CrossOptions(), @Recurse mainClass: MainClassOptions = MainClassOptions(), @Group(HelpGroup.Run.toString) @Hidden @Tag(tags.experimental) @Tag(tags.inShortHelp) @HelpMessage("Run as a Spark job, using the spark-submit command") @ExtraName("spark") sparkSubmit: Option[Boolean] = None, @Group(HelpGroup.Run.toString) @Hidden @Tag(tags.experimental) @HelpMessage("Spark-submit arguments") @ExtraName("submitArg") submitArgument: List[String] = Nil, @Group(HelpGroup.Run.toString) @Tag(tags.experimental) @HelpMessage("Run as a Spark job, using a vanilla Spark distribution downloaded by Scala CLI") @ExtraName("sparkStandalone") standaloneSpark: Option[Boolean] = None, @Group(HelpGroup.Run.toString) @Tag(tags.experimental) @HelpMessage("Run as a Hadoop job, using the \"hadoop jar\" command") @ExtraName("hadoop") hadoopJar: Boolean = false, @Group(HelpGroup.Run.toString) @Tag(tags.should) @Tag(tags.inShortHelp) @HelpMessage("Print the command that would have been run (one argument per line), rather than running it") command: Boolean = false, @Group(HelpGroup.Run.toString) @HelpMessage("Temporary / working directory where to write generated launchers") scratchDir: Option[String] = None, @Group(HelpGroup.Run.toString) @Hidden @Tag(tags.implementation) @HelpMessage("Run Java commands using a manifest-based class path (shortens command length)") useManifest: Option[Boolean] = None ) // format: on object SharedRunOptions { implicit lazy val parser: Parser[SharedRunOptions] = Parser.derive implicit lazy val help: Help[SharedRunOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/setupide/SetupIde.scala ================================================ package scala.cli.commands.setupide import caseapp.* import ch.epfl.scala.bsp4j.BspConnectionDetails import com.github.plokhotnyuk.jsoniter_scala.core.* import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker import com.google.gson.GsonBuilder import java.nio.charset.{Charset, StandardCharsets} import scala.build.* import scala.build.EitherCps.{either, value} import scala.build.bsp.IdeInputs import scala.build.errors.{BuildException, WorkspaceError} import scala.build.input.{Inputs, OnDisk, Virtual, WorkspaceOrigin} import scala.build.internal.Constants import scala.build.internals.EnvVar import scala.build.options.{BuildOptions, Scope} import scala.cli.CurrentParams import scala.cli.commands.CommandUtils.{hasSelfExecutablePreamble, isJar} import scala.cli.commands.shared.{SharedBspFileOptions, SharedOptions} import scala.cli.commands.util.JvmUtils import scala.cli.commands.{CommandUtils, ScalaCommand} import scala.cli.errors.FoundVirtualInputsError import scala.cli.launcher.LauncherOptions import scala.jdk.CollectionConverters.* object SetupIde extends ScalaCommand[SetupIdeOptions] { def downloadDeps( inputs: Inputs, options: BuildOptions, logger: Logger ): Either[BuildException, Artifacts] = { // ignoring errors related to sources themselves val maybeSourceBuildOptions = either { val (crossSources, allInputs) = value { CrossSources.forInputs( inputs, Sources.defaultPreprocessors( options.archiveCache, options.internal.javaClassNameVersionOpt, () => options.javaHome().value.javaCommand ), logger, options.suppressWarningOptions, options.internal.exclude, download = options.downloader ) } crossSources.sharedOptions(options) val scopedSources = value(crossSources.scopedSources(options)) val mainSources = value(scopedSources.sources( Scope.Main, crossSources.sharedOptions(options), allInputs.workspace, logger )) mainSources.buildOptions } val joinedBuildOpts = maybeSourceBuildOptions.toOption.map(options.orElse(_)).getOrElse(options) joinedBuildOpts.artifacts(logger, Scope.Main) } override def scalaSpecificationLevel = SpecificationLevel.IMPLEMENTATION override def runCommand(options: SetupIdeOptions, args: RemainingArgs, logger: Logger): Unit = { val buildOptions = buildOptionsOrExit(options) val inputs = options.shared.inputs(args.all).orExit(logger) CurrentParams.workspaceOpt = Some(inputs.workspace) val bspPath = writeBspConfiguration( options, inputs, buildOptions, previousCommandName = None, args = args.all ).orExit(logger) bspPath.foreach(path => println(s"Wrote configuration file for ide in: $path")) } def runSafe( options: SharedOptions, inputs: Inputs, logger: Logger, buildOptions: BuildOptions, previousCommandName: Option[String], args: Seq[String] ): Unit = writeBspConfiguration( SetupIdeOptions(shared = options), inputs, buildOptions, previousCommandName, args ) match { case Left(ex) => logger.debug(s"Ignoring error during setup-ide: ${ex.message}") case Right(_) => } override def sharedOptions(options: SetupIdeOptions): Option[SharedOptions] = Some(options.shared) private def writeBspConfiguration( options: SetupIdeOptions, inputs: Inputs, buildOptions: BuildOptions, previousCommandName: Option[String], args: Seq[String] ): Either[BuildException, Option[os.Path]] = either { val virtualInputs = inputs.elements.collect { case v: Virtual => v } if (virtualInputs.nonEmpty) value(Left(new FoundVirtualInputsError(virtualInputs))) val progName = argvOpt.flatMap(_.headOption).getOrElse { sys.error("setup-ide called in a non-standard way :|") } val logger = options.shared.logger if (buildOptions.classPathOptions.allExtraDependencies.toSeq.nonEmpty) value(downloadDeps( inputs, buildOptions, logger )) val (bspName, bspJsonDestination) = bspDetails(inputs.workspace, options.bspFile) val scalaCliBspJsonDestination = inputs.workspace / Constants.workspaceDirName / "ide-options-v2.json" val scalaCliBspLauncherOptsJsonDestination = inputs.workspace / Constants.workspaceDirName / "ide-launcher-options.json" val scalaCliBspInputsJsonDestination = inputs.workspace / Constants.workspaceDirName / "ide-inputs.json" val scalaCliBspEnvsJsonDestination = inputs.workspace / Constants.workspaceDirName / "ide-envs.json" val inputArgs = inputs.elements.collect { case d: OnDisk => d.path.toString } val ideInputs = IdeInputs( options.shared.validateInputArgs(args) .flatMap(_.toOption) .flatten .collect { case d: OnDisk => d.path.toString } ) val debugOpt = options.shared.jvm.bspDebugPort.toSeq.map(port => s"-J-agentlib:jdwp=transport=dt_socket,server=n,address=localhost:$port,suspend=y" ) val launcher = os.Path { launcherOptions.scalaRunner.initialLauncherPath .getOrElse(CommandUtils.getAbsolutePathToScalaCli(progName)) } val finalLauncherOptions = launcherOptions.copy(cliVersion = launcherOptions.cliVersion.orElse(launcherOptions.scalaRunner.predefinedCliVersion) ) val launcherCommand = if launcher.isJar && !launcher.hasSelfExecutablePreamble then List( value { JvmUtils.getJavaCmdVersionOrHigher( javaVersion = math.max(Constants.minimumInternalJavaVersion, Constants.minimumBloopJavaVersion), options = buildOptions ) }.javaCommand, "-jar", launcher.toString ) else List(launcher.toString) val bspArgs = launcherCommand ++ finalLauncherOptions.toCliArgs ++ launcherJavaPropArgs ++ List("bsp") ++ debugOpt ++ List("--json-options", scalaCliBspJsonDestination.toString) ++ List("--json-launcher-options", scalaCliBspLauncherOptsJsonDestination.toString) ++ List("--envs-file", scalaCliBspEnvsJsonDestination.toString) ++ inputArgs val details = new BspConnectionDetails( bspName, bspArgs.asJava, Constants.version, bloop.rifle.internal.BuildInfo.bspVersion, List("scala", "java").asJava ) val charset = options.charset .map(_.trim) .filter(_.nonEmpty) .map(Charset.forName) .getOrElse(StandardCharsets.UTF_8) val gson = new GsonBuilder().setPrettyPrinting().create() implicit val mapCodec: JsonValueCodec[Map[String, String]] = JsonCodecMaker.make val json = gson.toJson(details) val scalaCliOptionsForBspJson = writeToArray(options.shared)(using SharedOptions.jsonCodec) val scalaCliLaunchOptsForBspJson = writeToArray(finalLauncherOptions)(using LauncherOptions.jsonCodec) val scalaCliBspInputsJson = writeToArray(ideInputs) val envsForBsp = sys.env.filter((key, _) => EnvVar.allBsp.map(_.name).contains(key)) val scalaCliBspEnvsJson = writeToArray(envsForBsp) if (inputs.workspaceOrigin.contains(WorkspaceOrigin.HomeDir)) value(Left(new WorkspaceError( s"""$baseRunnerName can not determine where to write its BSP configuration. |Set an explicit BSP directory path via `--bsp-directory`. |""".stripMargin ))) if (previousCommandName.isEmpty || !bspJsonDestination.toIO.exists()) { os.write.over(bspJsonDestination, json.getBytes(charset), createFolders = true) os.write.over( scalaCliBspJsonDestination, scalaCliOptionsForBspJson, createFolders = true ) os.write.over( scalaCliBspLauncherOptsJsonDestination, scalaCliLaunchOptsForBspJson, createFolders = true ) os.write.over( scalaCliBspInputsJsonDestination, scalaCliBspInputsJson, createFolders = true ) os.write.over( scalaCliBspEnvsJsonDestination, scalaCliBspEnvsJson, createFolders = true ) logger.debug(s"Wrote $bspJsonDestination") Some(bspJsonDestination) } else None } def bspDetails(workspace: os.Path, ops: SharedBspFileOptions): (String, os.Path) = { import ops.* val dir = bspDirectory .filter(_.nonEmpty) .map(os.Path(_, Os.pwd)) .getOrElse(workspace / ".bsp") val bspName0 = bspName.map(_.trim).filter(_.nonEmpty).getOrElse(baseRunnerName) (bspName0, dir / s"$bspName0.json") } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/setupide/SetupIdeOptions.scala ================================================ package scala.cli.commands.setupide import caseapp.* import scala.cli.ScalaCli.{baseRunnerName, fullRunnerName} import scala.cli.commands.shared.{ HasSharedOptions, HelpMessages, SharedBspFileOptions, SharedOptions } import scala.cli.commands.tags @HelpMessage(SetupIdeOptions.helpMessage, "", SetupIdeOptions.detailedHelpMessage) // format: off final case class SetupIdeOptions( @Recurse shared: SharedOptions = SharedOptions(), @Recurse bspFile: SharedBspFileOptions = SharedBspFileOptions(), @Hidden @Tag(tags.implementation) charset: Option[String] = None ) extends HasSharedOptions // format: on object SetupIdeOptions { implicit lazy val parser: Parser[SetupIdeOptions] = Parser.derive implicit lazy val help: Help[SetupIdeOptions] = Help.derive val cmdName = "setup-ide" private val helpHeader = "Generates a BSP file that you can import into your IDE." val helpMessage: String = HelpMessages.shortHelpMessage(cmdName, helpHeader) val detailedHelpMessage: String = s"""$helpHeader | |The $cmdName sub-command allows to pre-configure a $fullRunnerName project to import to an IDE with BSP support. |It is also ran implicitly when `compile`, `run`, `shebang` or `test` sub-commands are called. | |The pre-configuration should be saved in a BSP json connection file under the path: | ${Console.BOLD}{project-root}/.bsp/$baseRunnerName.json${Console.RESET} | |${HelpMessages.commandConfigurations(cmdName)} | |${HelpMessages.commandDocWebsiteReference(cmdName)}""".stripMargin } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/AllExternalHelpOptions.scala ================================================ package scala.cli.commands.shared import caseapp.* import caseapp.core.help.Help import com.github.plokhotnyuk.jsoniter_scala.core.* import com.github.plokhotnyuk.jsoniter_scala.macros.* @HelpMessage("Print help message") // this is an aggregate for all external and internal help options case class AllExternalHelpOptions( @Recurse scalacExtra: ScalacExtraOptions = ScalacExtraOptions(), @Recurse helpGroups: HelpGroupOptions = HelpGroupOptions() ) object AllExternalHelpOptions { implicit lazy val parser: Parser[AllExternalHelpOptions] = Parser.derive implicit lazy val help: Help[AllExternalHelpOptions] = Help.derive implicit lazy val jsonCodec: JsonValueCodec[AllExternalHelpOptions] = JsonCodecMaker.make } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/ArgSplitter.scala ================================================ package scala.cli.commands.shared import scala.annotation.tailrec import scala.collection.mutable.ListBuffer object ArgSplitter { def splitToArgs(input: String) = { val iter = input.iterator val accumulator = new ListBuffer[String] @tailrec def takeWhile( test: Char => Boolean, acc: List[Char] = Nil, prevWasEscape: Boolean = false ): String = iter.nextOption() match case Some(c) if !prevWasEscape && test(c) => acc.reverse.mkString case None => acc.reverse.mkString case Some('\\') => takeWhile(test, '\\' :: acc, prevWasEscape = true) case Some(c) => takeWhile(test, c :: acc, prevWasEscape = false) while (iter.hasNext) iter.next() match case c if c.isSpaceChar || c == '\n' || c == '\r' => case c @ ('\'' | '"') => accumulator += s"$c${takeWhile(_ == c)}$c" case c => accumulator += s"$c${takeWhile(c => c.isSpaceChar || c == '\n' || c == '\r')}" accumulator.result() } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/BenchmarkingOptions.scala ================================================ package scala.cli.commands.shared import caseapp.* import scala.build.internal.Constants import scala.cli.commands.tags // format: off final case class BenchmarkingOptions( @Group(HelpGroup.Benchmarking.toString) @Tag(tags.experimental) @HelpMessage("Run JMH benchmarks") jmh: Option[Boolean] = None, @Group(HelpGroup.Benchmarking.toString) @Tag(tags.experimental) @HelpMessage(s"Set JMH version (default: ${Constants.jmhVersion})") @ValueDescription("version") jmhVersion: Option[String] = None ) // format: on object BenchmarkingOptions { implicit lazy val parser: Parser[BenchmarkingOptions] = Parser.derive implicit lazy val help: Help[BenchmarkingOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/CoursierOptions.scala ================================================ package scala.cli.commands.shared import caseapp.* import com.github.plokhotnyuk.jsoniter_scala.core.* import com.github.plokhotnyuk.jsoniter_scala.macros.* import coursier.cache.{CacheLogger, CachePolicy, FileCache} import coursier.util.Task import scala.build.Logger import scala.build.internals.EnvVar import scala.cli.commands.tags import scala.cli.config.Keys import scala.cli.util.ConfigDbUtils import scala.concurrent.duration.Duration // format: off final case class CoursierOptions( @Group(HelpGroup.Dependency.toString) @HelpMessage("Specify a TTL for changing dependencies, such as snapshots") @ValueDescription("duration|Inf") @Tag(tags.implementation) @Hidden ttl: Option[String] = None, @Group(HelpGroup.Dependency.toString) @HelpMessage("Set the coursier cache location") @ValueDescription("path") @Tag(tags.implementation) @Hidden cache: Option[String] = None, @Group(HelpGroup.Dependency.toString) @HelpMessage("Enable checksum validation of artifacts downloaded by coursier") @Tag(tags.implementation) @Hidden coursierValidateChecksums: Option[Boolean] = None, @Group(HelpGroup.Dependency.toString) @HelpMessage("Disable using the network to download artifacts, use the local cache only") @Tag(tags.experimental) offline: Option[Boolean] = None ) { // format: on private def validateChecksums = coursierValidateChecksums.getOrElse(true) def coursierCache(logger: Logger, cacheLogger: CacheLogger): FileCache[Task] = { var baseCache = FileCache().withLogger(cacheLogger) if (!validateChecksums) baseCache = baseCache.withChecksums(Nil) val ttlOpt = ttl.map(_.trim).filter(_.nonEmpty).map(Duration(_)) for (ttl0 <- ttlOpt) baseCache = baseCache.withTtl(ttl0) for (loc <- cache.filter(_.trim.nonEmpty)) baseCache = baseCache.withLocation(loc) for (isOffline <- getOffline(logger) if isOffline) baseCache = baseCache.withCachePolicies(Seq(CachePolicy.LocalOnly)) baseCache } def coursierCache(logger: Logger, cacheLoggerPrefix: String = ""): FileCache[Task] = coursierCache(logger, logger.coursierLogger(cacheLoggerPrefix)) def getOffline(logger: Logger): Option[Boolean] = offline .orElse(EnvVar.Coursier.coursierMode.valueOpt.map(_ == "offline")) .orElse(Option(System.getProperty("coursier.mode")).map(_ == "offline")) .orElse(ConfigDbUtils.getConfigDbOpt(logger).flatMap(_.get(Keys.offline).toOption.flatten)) } object CoursierOptions { implicit lazy val parser: Parser[CoursierOptions] = Parser.derive implicit lazy val help: Help[CoursierOptions] = Help.derive implicit lazy val jsonCodec: JsonValueCodec[CoursierOptions] = JsonCodecMaker.make } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/CrossOptions.scala ================================================ package scala.cli.commands.shared import caseapp.* import scala.cli.commands.tags // format: off final case class CrossOptions( @Tag(tags.experimental) @HelpMessage("Run given command against all provided Scala versions and/or platforms") cross: Option[Boolean] = None ) // format: on object CrossOptions { implicit lazy val parser: Parser[CrossOptions] = Parser.derive implicit lazy val help: Help[CrossOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/GlobalOptions.scala ================================================ package scala.cli.commands.shared import caseapp.* import scala.cli.launcher.PowerOptions case class GlobalOptions( @Recurse logging: LoggingOptions = LoggingOptions(), @Recurse globalSuppress: GlobalSuppressWarningOptions = GlobalSuppressWarningOptions(), /** Duplication of [[scala.cli.launcher.LauncherOptions.powerOptions]]. Thanks to this, our unit * tests ensure that no subcommand defines an option that will clash with --power. */ @Recurse powerOptions: PowerOptions = PowerOptions() ) object GlobalOptions { implicit lazy val parser: Parser[GlobalOptions] = Parser.derive implicit lazy val help: Help[GlobalOptions] = Help.derive lazy val default: GlobalOptions = GlobalOptions() def get(args: List[String]): Option[GlobalOptions] = parser .detailedParse(args, stopAtFirstUnrecognized = false, ignoreUnrecognized = true) .toOption .map(_._1) } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/GlobalSuppressWarningOptions.scala ================================================ package scala.cli.commands.shared import caseapp.* import scala.cli.commands.tags final case class GlobalSuppressWarningOptions( @Group(HelpGroup.SuppressWarnings.toString) @Tag(tags.implementation) @HelpMessage("Suppress warnings about using experimental features") @Name("suppressExperimentalWarning") suppressExperimentalFeatureWarning: Option[Boolean] = None, @Group(HelpGroup.SuppressWarnings.toString) @Tag(tags.implementation) @HelpMessage("Suppress warnings about using deprecated features") @Name("suppressDeprecatedWarning") @Name("suppressDeprecatedWarnings") @Name("suppressDeprecatedFeatureWarnings") suppressDeprecatedFeatureWarning: Option[Boolean] = None ) object GlobalSuppressWarningOptions { implicit lazy val parser: Parser[GlobalSuppressWarningOptions] = Parser.derive implicit lazy val help: Help[GlobalSuppressWarningOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/HasGlobalOptions.scala ================================================ package scala.cli.commands.shared trait HasGlobalOptions { def global: GlobalOptions } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/HasSharedOptions.scala ================================================ package scala.cli.commands.shared trait HasSharedOptions extends HasGlobalOptions { def shared: SharedOptions override def global: GlobalOptions = shared.global } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/HasSharedWatchOptions.scala ================================================ package scala.cli.commands.shared import scala.build.errors.BuildException trait HasSharedWatchOptions { this: HasSharedOptions => def watch: SharedWatchOptions def buildOptions(ignoreErrors: Boolean = false): Either[BuildException, scala.build.options.BuildOptions] = shared.buildOptions(ignoreErrors = ignoreErrors, watchOptions = watch) } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/HelpGroupOptions.scala ================================================ package scala.cli.commands.shared import caseapp.* import caseapp.core.Scala3Helpers.* import caseapp.core.help.{Help, HelpFormat} import com.github.plokhotnyuk.jsoniter_scala.core.* import com.github.plokhotnyuk.jsoniter_scala.macros.* import scala.cli.commands.tags @HelpMessage("Print help message") case class HelpGroupOptions( @Group(HelpGroup.Help.toString) @HelpMessage("Show environment variable help") @Tag(tags.implementation) @Tag(tags.inShortHelp) @Name("helpEnv") @Name("envHelp") @Name("envsHelp") helpEnvs: Boolean = false, @Group(HelpGroup.Help.toString) @HelpMessage("Show options for ScalaJS") @Tag(tags.implementation) @Tag(tags.inShortHelp) helpJs: Boolean = false, @Group(HelpGroup.Help.toString) @HelpMessage("Show options for ScalaNative") @Tag(tags.implementation) @Tag(tags.inShortHelp) helpNative: Boolean = false, @Group(HelpGroup.Help.toString) @HelpMessage("Show options for Scaladoc") @Name("helpDoc") @Name("scaladocHelp") @Name("docHelp") @Tag(tags.implementation) @Tag(tags.inShortHelp) helpScaladoc: Boolean = false, @Group(HelpGroup.Help.toString) @HelpMessage("Show options for Scala REPL") @Name("replHelp") @Tag(tags.implementation) @Tag(tags.inShortHelp) helpRepl: Boolean = false, @Group(HelpGroup.Help.toString) @HelpMessage("Show options for Scalafmt") @Name("helpFmt") @Name("scalafmtHelp") @Name("fmtHelp") @Tag(tags.implementation) @Tag(tags.inShortHelp) helpScalafmt: Boolean = false ) { private def printHelpWithGroup(help: Help[?], helpFormat: HelpFormat, group: String): Nothing = { val oldHiddenGroups = helpFormat.hiddenGroups.toSeq.flatten val oldSortedGroups = helpFormat.sortedGroups.toSeq.flatten val newHiddenGroups = (oldHiddenGroups ++ oldSortedGroups).filterNot(_ == group) println( help.help( helpFormat.withHiddenGroupsWhenShowHidden(Some(newHiddenGroups)), showHidden = true ) ) sys.exit(0) } def maybePrintGroupHelp(help: Help[?], helpFormat: HelpFormat): Unit = { if (helpJs) printHelpWithGroup(help, helpFormat, HelpGroup.ScalaJs.toString) else if (helpNative) printHelpWithGroup(help, helpFormat, HelpGroup.ScalaNative.toString) } } object HelpGroupOptions { implicit lazy val parser: Parser[HelpGroupOptions] = Parser.derive implicit lazy val help: Help[HelpGroupOptions] = Help.derive implicit lazy val jsonCodec: JsonValueCodec[HelpGroupOptions] = JsonCodecMaker.make } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/HelpGroups.scala ================================================ package scala.cli.commands.shared enum HelpGroup: case Benchmarking, BSP, BuildToolExport, Config, Compilation, CompilationServer, Debian, Debug, Default, Dependency, Doc, Docker, Entrypoint, Fix, Format, Help, Install, Java, Launcher, LegacyScalaRunner, Logging, MacOS, Markdown, NativeImage, Package, PGP, ProjectVersion, Publishing, RedHat, Repl, Run, Runner, Scala, ScalaJs, ScalaNative, Secret, Signing, SuppressWarnings, SourceGenerator, Test, Uninstall, Update, Watch, Windows, Version override def toString: String = this match case BuildToolExport => "Build Tool export" case CompilationServer => "Compilation server" case LegacyScalaRunner => "Legacy Scala runner" case NativeImage => "Native image" case ScalaJs => "Scala.js" case ScalaNative => "Scala Native" case SuppressWarnings => "Suppress warnings" case SourceGenerator => "Source generator" case ProjectVersion => "Project version" case e => e.productPrefix enum HelpCommandGroup: case Main, Miscellaneous, Undefined override def toString: String = this match case Undefined => "" case e => e.productPrefix ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/HelpMessages.scala ================================================ package scala.cli.commands.shared import scala.cli.ScalaCli object HelpMessages { lazy val PowerString: String = if ScalaCli.allowRestrictedFeatures then "" else "--power " val passwordOption = "A github token used to access GitHub. Not needed in most cases." private val docsWebsiteUrl = "https://scala-cli.virtuslab.org" def shortHelpMessage( cmdName: String, helpHeader: String, includeFullHelpReference: Boolean = true, needsPower: Boolean = false ): String = { val maybeFullHelpReference = if includeFullHelpReference then s""" |${HelpMessages.commandFullHelpReference(cmdName, needsPower)}""".stripMargin else "" s"""$helpHeader |$maybeFullHelpReference |${HelpMessages.commandDocWebsiteReference(cmdName)}""".stripMargin } val docsWebsiteReference = s"Detailed documentation can be found on our website: $docsWebsiteUrl" def commandFullHelpReference(commandName: String, needsPower: Boolean = false): String = { val maybePowerString = if needsPower then "--power " else "" s"""You are currently viewing the basic help for the $commandName sub-command. You can view the full help by running: | ${Console.BOLD}${ScalaCli.progName} $maybePowerString$commandName --help-full${Console .RESET}""".stripMargin } def commandDocWebsiteReference(websiteSuffix: String): String = s"For detailed documentation refer to our website: $docsWebsiteUrl/docs/commands/$websiteSuffix" val installationDocsWebsiteReference = s"For detailed installation instructions refer to our website: $docsWebsiteUrl/install" val acceptedInputs: String = """Multiple inputs can be passed at once. |Paths to directories, URLs and supported file types are accepted as inputs. |Accepted file extensions: .scala, .sc, .java, .jar, .md, .jar, .c, .h, .zip |For piped inputs use the corresponding alias: _.scala, _.java, _.sc, _.md |All supported types of inputs can be mixed with each other.""".stripMargin lazy val bloopInfo: String = s"""Bloop is the build server used by ${ScalaCli.fullRunnerName}. |For more information about Bloop, refer to https://scalacenter.github.io/bloop/""".stripMargin def commandConfigurations(cmdName: String): String = s"""Specific $cmdName configurations can be specified with both command line options and using directives defined in sources. |Command line options always take priority over using directives when a clash occurs, allowing to override configurations defined in sources. |Using directives can be defined in all supported input source file types.""".stripMargin } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/HelpOptions.scala ================================================ package scala.cli.commands.shared import caseapp.* // format: off @HelpMessage("Print help message") case class HelpOptions( @Recurse global: GlobalOptions = GlobalOptions(), ) extends HasGlobalOptions // format: on object HelpOptions { implicit lazy val parser: Parser[HelpOptions] = Parser.derive implicit lazy val help: Help[HelpOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/JavaPropOptions.scala ================================================ package scala.cli.commands.shared import caseapp.* import caseapp.core.parser.{Argument, NilParser, StandardArgument} import caseapp.core.util.Formatter import caseapp.core.{Arg, Error} import com.github.plokhotnyuk.jsoniter_scala.core.* import com.github.plokhotnyuk.jsoniter_scala.macros.* import scala.cli.commands.tags // format: off final case class JavaPropOptions( @Group(HelpGroup.Java.toString) @HelpMessage("Set java properties") @ValueDescription("key=value|key") @Tag(tags.must) javaProp: List[String] = Nil ) // format: on object JavaPropOptions { private val javaPropOptionsArg = Arg("javaPropOption").copy( extraNames = Seq(Name("java-prop")), valueDescription = Some(ValueDescription("key=value|key")), helpMessage = Some(HelpMessage( "Add java properties. Note that options equal `-Dproperty=value` are assumed to be java properties and don't require to be passed after `--java-prop`." )), group = Some(Group("Java")), origin = Some("JavaPropOptions") ) private val javaPropOptionsArgument: Argument[List[String]] = new Argument[List[String]] { val underlying: StandardArgument[List[String]] = StandardArgument(javaPropOptionsArg) val arg: Arg = javaPropOptionsArg def withDefaultOrigin(origin: String): Argument[List[String]] = this def init: Option[List[String]] = Some(Nil) def step( args: List[String], index: Int, acc: Option[List[String]], formatter: Formatter[Name] ): Either[(Error, List[String]), Option[(Option[List[String]], List[String])]] = args match { case s"-D${prop}" :: t => Right(Some((Some(prop :: acc.getOrElse(Nil)), t))) case _ => underlying.step(args, index, acc, formatter) } def get(acc: Option[List[String]], formatter: Formatter[Name]): Either[Error, List[String]] = Right(acc.getOrElse(Nil)) } implicit lazy val parser: Parser[JavaPropOptions] = { val baseParser = javaPropOptionsArgument :: NilParser baseParser.to[JavaPropOptions] } implicit lazy val help: Help[JavaPropOptions] = Help.derive implicit lazy val jsonCodec: JsonValueCodec[JavaPropOptions] = JsonCodecMaker.make } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/LoggingOptions.scala ================================================ package scala.cli.commands.shared import caseapp.* import com.github.plokhotnyuk.jsoniter_scala.core.* import com.github.plokhotnyuk.jsoniter_scala.macros.* import scala.build.Logger import scala.cli.commands.tags import scala.cli.internal.CliLogger // format: off final case class LoggingOptions( @Recurse verbosityOptions: VerbosityOptions = VerbosityOptions(), @Group(HelpGroup.Logging.toString) @HelpMessage("Decrease logging verbosity") @Tag(tags.implementation) @Name("q") quiet: Boolean = false, @Group(HelpGroup.Logging.toString) @Tag(tags.implementation) @HelpMessage("Use progress bars") progress: Option[Boolean] = None ) { // format: on lazy val verbosity = verbosityOptions.verbosity - (if (quiet) 1 else 0) lazy val logger: Logger = new CliLogger(verbosity, quiet, progress, System.err) } object LoggingOptions { implicit lazy val parser: Parser[LoggingOptions] = Parser.derive implicit lazy val help: Help[LoggingOptions] = Help.derive implicit lazy val jsonCodec: JsonValueCodec[LoggingOptions] = JsonCodecMaker.make } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/MainClassOptions.scala ================================================ package scala.cli.commands.shared import caseapp.* import scala.build.errors.{MainClassError, NoMainClassFoundError} import scala.cli.commands.tags // format: off final case class MainClassOptions( @Group(HelpGroup.Entrypoint.toString) @HelpMessage("Specify which main class to run") @ValueDescription("main-class") @Tag(tags.must) @Name("M") mainClass: Option[String] = None, @Group(HelpGroup.Entrypoint.toString) @HelpMessage("List main classes available in the current context") @Name("mainClassList") @Name("listMainClass") @Name("listMainClasses") @Name("listMainMethods") @Name("listMainMethod") @Name("mainMethodList") @Name("mainMethodLs") @Tag(tags.should) @Tag(tags.inShortHelp) mainClassLs: Option[Boolean] = None ) { // format: on def maybePrintMainClasses( mainClasses: Seq[String], shouldExit: Boolean = true ): Either[MainClassError, Unit] = mainClassLs match { case Some(true) if mainClasses.nonEmpty => println(mainClasses.mkString(" ")) if (shouldExit) sys.exit(0) else Right(()) case Some(true) => Left(new NoMainClassFoundError) case _ => Right(()) } } object MainClassOptions { implicit lazy val parser: Parser[MainClassOptions] = Parser.derive implicit lazy val help: Help[MainClassOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/MarkdownOptions.scala ================================================ package scala.cli.commands.shared import caseapp.* import scala.cli.commands.tags // format: off final case class MarkdownOptions( @Group(HelpGroup.Markdown.toString) @Tag(tags.experimental) @HelpMessage("Enable markdown support.") @Name("md") @Name("markdown") enableMarkdown: Boolean = false // TODO: add a separate scope for Markdown and remove this option once it's stable ) // format: on object MarkdownOptions { implicit lazy val parser: Parser[MarkdownOptions] = Parser.derive implicit lazy val help: Help[MarkdownOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/ScalaCliHelp.scala ================================================ package scala.cli.commands.shared import caseapp.core.help.HelpFormat import scala.cli.util.ArgHelpers.* import scala.util.{Properties, Try} object ScalaCliHelp { private val sortedHelpGroups = Seq( HelpGroup.Scala, HelpGroup.Java, HelpGroup.Watch, HelpGroup.Dependency, HelpGroup.Entrypoint, HelpGroup.Debug, HelpGroup.Repl, HelpGroup.Run, HelpGroup.Package, HelpGroup.CompilationServer, HelpGroup.Logging, HelpGroup.Runner, HelpGroup.Launcher, HelpGroup.LegacyScalaRunner, HelpGroup.ScalaJs, HelpGroup.ScalaNative, HelpGroup.Help ) private val hiddenHelpGroups = Seq(HelpGroup.ScalaJs, HelpGroup.ScalaNative) private val sortedCommandGroups = Seq(HelpCommandGroup.Main, HelpCommandGroup.Miscellaneous, HelpCommandGroup.Undefined) val helpFormat: HelpFormat = HelpFormat.default() .copy( filterArgs = Some(arg => arg.isSupported && (arg.isMust || arg.isImportant)), filterArgsWhenShowHidden = Some(_.isSupported), terminalWidthOpt = if (Properties.isWin) if (coursier.paths.Util.useJni()) Try(coursier.jniutils.WindowsAnsiTerminal.terminalSize()).toOption.map( _.getWidth ).orElse { val fallback = 120 if (java.lang.Boolean.getBoolean("scala.cli.windows-terminal.verbose")) System.err.println(s"Could not get terminal width, falling back to $fallback") Some(fallback) } else None else // That's how Ammonite gets the terminal width, but I'd rather not spawn a sub-process upfront in Scala CLI… // val pathedTput = if (os.isFile(os.Path("/usr/bin/tput"))) "/usr/bin/tput" else "tput" // val width = os.proc("sh", "-c", s"$pathedTput cols 2>/dev/tty").call(stderr = os.Pipe).out.trim().toInt // Some(width) // Ideally, we should do an ioctl, like jansi does here: // https://github.com/fusesource/jansi/blob/09722b7cccc8a99f14ac1656db3072dbeef34478/src/main/java/org/fusesource/jansi/AnsiConsole.java#L344 // This requires writing our own minimal JNI library, that publishes '.a' files too for static linking in the executable of Scala CLI. None ) .withSortedCommandGroups(sortedCommandGroups) .withSortedGroups(sortedHelpGroups) .withHiddenGroups(hiddenHelpGroups) .withNamesLimit(2) } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/ScalaJsOptions.scala ================================================ package scala.cli.commands.shared import caseapp.* import com.github.plokhotnyuk.jsoniter_scala.core.* import com.github.plokhotnyuk.jsoniter_scala.macros.* import scala.cli.commands.{Constants, tags} // format: off final case class ScalaJsOptions( @Group(HelpGroup.Scala.toString) @Tag(tags.should) @HelpMessage("Enable Scala.js. To show more options for Scala.js pass `--help-js`") js: Boolean = false, @Group(HelpGroup.ScalaJs.toString) @Tag(tags.should) @HelpMessage(s"The Scala.js version (${Constants.scalaJsVersion} by default).") jsVersion: Option[String] = None, @Group(HelpGroup.ScalaJs.toString) @Tag(tags.should) @HelpMessage("The Scala.js mode, for `fastLinkJS` use one of [`dev`, `fastLinkJS` or `fast`], for `fullLinkJS` use one of [`release`, `fullLinkJS`, `full`]") jsMode: Option[String] = None, @Group(HelpGroup.ScalaJs.toString) @HelpMessage("Disable optimalisation for Scala.js, overrides `--js-mode`") @Tag(tags.implementation) @Hidden jsNoOpt: Option[Boolean] = None, @HelpMessage("The Scala.js module kind: commonjs/common, esmodule/es, nomodule/none") @Group(HelpGroup.ScalaJs.toString) @Tag(tags.should) jsModuleKind: Option[String] = None, @Group(HelpGroup.ScalaJs.toString) @Tag(tags.should) jsCheckIr: Option[Boolean] = None, @Group(HelpGroup.ScalaJs.toString) @HelpMessage("Emit source maps") @Tag(tags.should) jsEmitSourceMaps: Boolean = false, @Group(HelpGroup.ScalaJs.toString) @HelpMessage("Set the destination path of source maps") @Tag(tags.should) jsSourceMapsPath: Option[String] = None, @Group(HelpGroup.ScalaJs.toString) @HelpMessage("A file relative to the root directory containing import maps for ES module imports") @Tag(tags.should) jsEsModuleImportMap: Option[String] = None, @Group(HelpGroup.ScalaJs.toString) @Tag(tags.should) @HelpMessage("Enable jsdom") jsDom: Option[Boolean] = None, @Group(HelpGroup.ScalaJs.toString) @Tag(tags.experimental) @HelpMessage("Emit WASM") jsEmitWasm: Option[Boolean] = None, @Group(HelpGroup.ScalaJs.toString) @Tag(tags.should) @HelpMessage("A header that will be added at the top of generated .js files") jsHeader: Option[String] = None, @Group(HelpGroup.ScalaJs.toString) @Tag(tags.implementation) @HelpMessage("Primitive Longs *may* be compiled as primitive JavaScript bigints") jsAllowBigIntsForLongs: Option[Boolean] = None, @Group(HelpGroup.ScalaJs.toString) @Tag(tags.implementation) @HelpMessage("Avoid class'es when using functions and prototypes has the same observable semantics.") jsAvoidClasses: Option[Boolean] = None, @Group(HelpGroup.ScalaJs.toString) @Tag(tags.implementation) @HelpMessage("Avoid lets and consts when using vars has the same observable semantics.") jsAvoidLetsAndConsts: Option[Boolean] = None, @Group(HelpGroup.ScalaJs.toString) @Tag(tags.implementation) @HelpMessage("The Scala.js module split style: fewestmodules, smallestmodules, smallmodulesfor") jsModuleSplitStyle: Option[String] = None, @Group(HelpGroup.ScalaJs.toString) @Tag(tags.implementation) @HelpMessage("Create as many small modules as possible for the classes in the passed packages and their subpackages.") jsSmallModuleForPackage: List[String] = Nil, @Group(HelpGroup.ScalaJs.toString) @Tag(tags.should) @HelpMessage("The Scala.js ECMA Script version: es5_1, es2015, es2016, es2017, es2018, es2019, es2020, es2021") jsEsVersion: Option[String] = None, @Group(HelpGroup.ScalaJs.toString) @HelpMessage("Path to the Scala.js linker") @ValueDescription("path") @Tag(tags.implementation) @Hidden jsLinkerPath: Option[String] = None, @Group(HelpGroup.ScalaJs.toString) @HelpMessage(s"Scala.js CLI version to use for linking (${Constants.scalaJsCliVersion} by default).") @ValueDescription("version") @Tag(tags.implementation) @Hidden jsCliVersion: Option[String] = None, @Group(HelpGroup.ScalaJs.toString) @HelpMessage("Scala.js CLI Java options") @Tag(tags.implementation) @ValueDescription("option") @Hidden jsCliJavaArg: List[String] = Nil, @Group(HelpGroup.ScalaJs.toString) @HelpMessage("Whether to run the Scala.js CLI on the JVM or using a native executable") @Tag(tags.implementation) @Hidden jsCliOnJvm: Option[Boolean] = None ) // format: on object ScalaJsOptions { implicit lazy val parser: Parser[ScalaJsOptions] = Parser.derive implicit lazy val help: Help[ScalaJsOptions] = Help.derive implicit lazy val jsonCodec: JsonValueCodec[ScalaJsOptions] = JsonCodecMaker.make } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/ScalaNativeOptions.scala ================================================ package scala.cli.commands.shared import caseapp.* import com.github.plokhotnyuk.jsoniter_scala.core.* import com.github.plokhotnyuk.jsoniter_scala.macros.* import scala.cli.commands.{Constants, tags} // format: off final case class ScalaNativeOptions( @Group(HelpGroup.Scala.toString) @HelpMessage("Enable Scala Native. To show more options for Scala Native pass `--help-native`") @Tag(tags.should) native: Boolean = false, @Group(HelpGroup.ScalaNative.toString) @Tag(tags.should) @HelpMessage(s"Set the Scala Native version (${Constants.scalaNativeVersion} by default).") nativeVersion: Option[String] = None, @Group(HelpGroup.ScalaNative.toString) @HelpMessage("Set Scala Native compilation mode (debug by default): debug, release-fast, release-size, release-full") @Tag(tags.should) nativeMode: Option[String] = None, @Group(HelpGroup.ScalaNative.toString) @HelpMessage("Link-time optimisation mode (none by default): none, full, thin") @Tag(tags.should) nativeLto: Option[String] = None, @Group(HelpGroup.ScalaNative.toString) @HelpMessage("Set the Scala Native garbage collector (immix by default): immix, commix, boehm, none") @Tag(tags.should) nativeGc: Option[String] = None, @Group(HelpGroup.ScalaNative.toString) @HelpMessage("Path to the Clang command") @Tag(tags.implementation) nativeClang: Option[String] = None, @Group(HelpGroup.ScalaNative.toString) @HelpMessage("Path to the Clang++ command") @Tag(tags.implementation) nativeClangpp: Option[String] = None, @Group(HelpGroup.ScalaNative.toString) @HelpMessage("Extra options passed to `clang` verbatim during linking") @Tag(tags.should) nativeLinking: List[String] = Nil, @Group(HelpGroup.ScalaNative.toString) @HelpMessage("Use default linking settings") @Hidden @Tag(tags.implementation) nativeLinkingDefaults: Option[Boolean] = None, //TODO does it even work when we default it to true while handling? @Group(HelpGroup.ScalaNative.toString) @HelpMessage("List of compile options") @Tag(tags.should) nativeCompile: List[String] = Nil, @Group(HelpGroup.ScalaNative.toString) @HelpMessage("List of compile options (C files only)") @Tag(tags.should) nativeCCompile: List[String] = Nil, @Group(HelpGroup.ScalaNative.toString) @HelpMessage("List of compile options (C++ files only)") @Tag(tags.should) nativeCppCompile: List[String] = Nil, @Group(HelpGroup.ScalaNative.toString) @Hidden @HelpMessage("Use default compile options") @Tag(tags.implementation) nativeCompileDefaults: Option[Boolean] = None, //TODO does it even work when we default it to true while handling? @Group(HelpGroup.ScalaNative.toString) @HelpMessage("Build target type") @Tag(tags.should) @ValueDescription("app|static|dynamic") nativeTarget: Option[String] = None, @Group(HelpGroup.ScalaNative.toString) @HelpMessage("Embed resources into the Scala Native binary (can be read with the Java resources API)") @Tag(tags.should) embedResources: Option[Boolean] = None, @Group(HelpGroup.ScalaNative.toString) @HelpMessage("Enable/disable Scala Native multithreading support") @Tag(tags.should) nativeMultithreading: Option[Boolean] = None ) // format: on object ScalaNativeOptions { implicit lazy val parser: Parser[ScalaNativeOptions] = Parser.derive implicit lazy val help: Help[ScalaNativeOptions] = Help.derive implicit lazy val jsonCodec: JsonValueCodec[ScalaNativeOptions] = JsonCodecMaker.make } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/ScalacExtraOptions.scala ================================================ package scala.cli.commands.shared import caseapp.* import com.github.plokhotnyuk.jsoniter_scala.core.* import com.github.plokhotnyuk.jsoniter_scala.macros.* import scala.cli.commands.tags /** Scala CLI options which aren't strictly scalac options, but directly involve the Scala compiler * in some way. */ // format: off final case class ScalacExtraOptions( @Group(HelpGroup.Scala.toString) @HelpMessage("Show help for scalac. This is an alias for --scalac-option -help") @Name("helpScalac") @Tag(tags.inShortHelp) scalacHelp: Boolean = false, @Group(HelpGroup.Scala.toString) @HelpMessage("Turn verbosity on for scalac. This is an alias for --scalac-option -verbose") @Name("verboseScalac") @Tag(tags.inShortHelp) scalacVerbose: Boolean = false, ) // format: on object ScalacExtraOptions { implicit lazy val parser: Parser[ScalacExtraOptions] = Parser.derive implicit lazy val help: Help[ScalacExtraOptions] = Help.derive implicit lazy val jsonCodec: JsonValueCodec[ScalacExtraOptions] = JsonCodecMaker.make } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/ScalacOptions.scala ================================================ package scala.cli.commands.shared import caseapp.* import caseapp.core.parser.{Argument, NilParser, StandardArgument} import caseapp.core.util.Formatter import caseapp.core.{Arg, Error} import com.github.plokhotnyuk.jsoniter_scala.core.* import com.github.plokhotnyuk.jsoniter_scala.macros.* import scala.build.options.ScalacOpt.noDashPrefixes import scala.cli.commands.tags // format: off final case class ScalacOptions( @Recurse argsFiles: List[ArgFileOption] = Nil, @Group(HelpGroup.Scala.toString) @HelpMessage("Add a scalac option") @ValueDescription("option") @Name("O") @Name("scala-opt") @Name("scala-option") @Tag(tags.must) scalacOption: List[String] = Nil, ) // format: on object ScalacOptions { extension (opt: String) { private def hasValidScalacOptionDashes: Boolean = opt.startsWith("-") && opt.length > 1 && ( if opt.length > 2 then opt.charAt(2) != '-' else opt.charAt(1) != '-' ) } private val scalacOptionsArg = Arg("scalacOption").copy( extraNames = Seq(Name("scala-opt"), Name("O"), Name("scala-option")), valueDescription = Some(ValueDescription("option")), helpMessage = Some(HelpMessage( "Add a `scalac` option. Note that options starting with `-g`, `-language`, `-opt`, `-P`, `-target`, `-V`, `-W`, `-X`, and `-Y` are assumed to be Scala compiler options and don't require to be passed after `-O` or `--scalac-option`." )), group = Some(Group("Scala")), origin = Some("ScalacOptions") ) // .withIsFlag(true) // The scalac options we handle accept no value after the -… argument val YScriptRunnerOption = "Yscriptrunner" private val scalacOptionsPurePrefixes = Set("V", "W", "X", "Y") private val scalacOptionsPrefixes = Set("P") ++ scalacOptionsPurePrefixes val replExecuteScriptOptions @ Seq(replInitScript, replQuitAfterInit) = Seq("repl-init-script", "repl-quit-after-init") private val replAliasedOptions = Set(replInitScript) private val replNoArgAliasedOptions = Set(replQuitAfterInit) private val scalacAliasedOptions = // these options don't require being passed after -O and accept an arg Set( "bootclasspath", "boot-class-path", "coverage-exclude-classlikes", "coverage-exclude-files", "encoding", "extdirs", "extension-directories", "javabootclasspath", "java-boot-class-path", "javaextdirs", "java-extension-directories", "java-output-version", "release", "color", "g", "language", "opt", "opt-inline", "pagewidth", "page-width", "target", "scalajs-mapSourceURI", "scalajs-genStaticForwardersForNonTopLevelObjects", "source", "sourcepath", "source-path", "sourceroot", YScriptRunnerOption ) ++ replAliasedOptions private val scalacNoArgAliasedOptions = // these options don't require being passed after -O and don't accept an arg Set( "experimental", "explain", "explaintypes", "explain-types", "explain-cyclic", "from-tasty", "unchecked", "nowarn", "no-warnings", "feature", "deprecation", "rewrite", "scalajs", "old-syntax", "print-tasty", "print-lines", "new-syntax", "indent", "no-indent", "preview", "uniqid", "unique-id" ) ++ replNoArgAliasedOptions /** True when the token ends with a `:help` suffix, with any number of colon-separated segments * before it (e.g. `Xlint:help`, `opt:a:b:c:help`). Used together with `ScalacPrintOptions` so * new compiler `…:help` flags work without listing each one. */ def isColonHelpPrintOption(noDashPrefixes: String): Boolean = noDashPrefixes.endsWith(":help") /** `scalac` options that print help or context and exit without requiring source inputs. */ val ScalacPrintOptions: Set[String] = scalacOptionsPurePrefixes ++ Set( "help", "Xshow-phases", "Xplugin-list", "Vphases" ) /** Whether `ScalaCommand.maybePrintSimpleScalacOutput` should run for this token (after * `ScalacOpt.noDashPrefixes`). */ def isScalacPrintOption(noDashPrefixes: String): Boolean = ScalacPrintOptions.contains(noDashPrefixes) || isColonHelpPrintOption(noDashPrefixes) /** This includes all the scalac options which are redirected to native Scala CLI options. */ val ScalaCliRedirectedOptions: Set[String] = Set( "classpath", "cp", // redirected to --extra-jars "class-path", // redirected to --extra-jars "d" // redirected to --compilation-output ) val ScalacDeprecatedOptions: Set[String] = Set( YScriptRunnerOption // old 'scala' runner specific, no longer supported ) private val scalacOptionsArgument: Argument[List[String]] = new Argument[List[String]] { val underlying: StandardArgument[List[String]] = StandardArgument(scalacOptionsArg) val arg: Arg = scalacOptionsArg def withDefaultOrigin(origin: String): Argument[List[String]] = this def init: Option[List[String]] = Some(Nil) def step( args: List[String], index: Int, acc: Option[List[String]], formatter: Formatter[Name] ): Either[(Error, List[String]), Option[(Option[List[String]], List[String])]] = args match { case h :: t if h.hasValidScalacOptionDashes && scalacOptionsPrefixes.exists(h.noDashPrefixes.startsWith) && !ScalacDeprecatedOptions.contains(h.noDashPrefixes) => Right(Some((Some(acc.getOrElse(Nil) :+ h), t))) case h :: t if h.hasValidScalacOptionDashes && scalacNoArgAliasedOptions.contains(h.noDashPrefixes) => Right(Some((Some(acc.getOrElse(Nil) :+ h), t))) case h :: t if h.hasValidScalacOptionDashes && scalacAliasedOptions.exists(o => h.noDashPrefixes.startsWith(o + ":")) && h.count(_ == ':') == 1 => Right(Some((Some(acc.getOrElse(Nil) :+ h), t))) case h :: t if h.hasValidScalacOptionDashes && scalacAliasedOptions.contains(h.noDashPrefixes) => // check if the next scalac arg is a different option or a param to the current option val maybeOptionArg = t.headOption.filter(!_.startsWith("-")) // if it's a param, it'll be treated as such and considered already parsed val newTail = maybeOptionArg.map(_ => t.drop(1)).getOrElse(t) val newHead = List(h) ++ maybeOptionArg Right(Some((Some(acc.getOrElse(Nil) ++ newHead), newTail))) case _ => underlying.step(args, index, acc, formatter) } def get(acc: Option[List[String]], formatter: Formatter[Name]): Either[Error, List[String]] = Right(acc.getOrElse(Nil)) } implicit lazy val parser: Parser[ScalacOptions] = { val baseParser = scalacOptionsArgument :: NilParser implicit val p: Parser[List[ArgFileOption]] = ArgFileOption.parser baseParser.addAll[List[ArgFileOption]].to[ScalacOptions] } implicit lazy val help: Help[ScalacOptions] = Help.derive implicit lazy val jsonCodec: JsonValueCodec[ScalacOptions] = JsonCodecMaker.make } case class ArgFileOption(file: String) extends AnyVal object ArgFileOption { val arg: Arg = Arg( name = Name("args-file"), valueDescription = Some(ValueDescription("@arguments-file")), helpMessage = Some(HelpMessage("File with scalac options.")), group = Some(Group("Scala")), origin = Some("ScalacOptions") ) implicit lazy val parser: Parser[List[ArgFileOption]] = new Parser[List[ArgFileOption]] { type D = List[ArgFileOption] *: EmptyTuple override def withDefaultOrigin(origin: String): Parser[List[ArgFileOption]] = this override def init: D = Nil *: EmptyTuple override def step(args: List[String], index: Int, d: D, nameFormatter: Formatter[Name]) : Either[(core.Error, Arg, List[String]), Option[(D, Arg, List[String])]] = args match case head :: rest if head.startsWith("@") => val newD = (ArgFileOption(head.stripPrefix("@")) :: d._1) *: EmptyTuple Right(Some(newD, arg, rest)) case _ => Right(None) override def get( d: D, nameFormatter: Formatter[Name] ): Either[core.Error, List[ArgFileOption]] = Right(d.head) override def args: Seq[Arg] = Seq(arg) } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/ScopeOptions.scala ================================================ package scala.cli.commands.shared import caseapp.* import scala.cli.commands.tags case class ScopeOptions( @Group(HelpGroup.Compilation.toString) @HelpMessage("Include test scope") @Tag(tags.should) @Tag(tags.inShortHelp) @Name("testScope") @Name("withTestScope") @Name("withTest") test: Option[Boolean] = None ) object ScopeOptions { implicit lazy val parser: Parser[ScopeOptions] = Parser.derive implicit lazy val help: Help[ScopeOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/SemanticDbOptions.scala ================================================ package scala.cli.commands.shared import caseapp.* import com.github.plokhotnyuk.jsoniter_scala.core.* import com.github.plokhotnyuk.jsoniter_scala.macros.* import scala.cli.commands.tags case class SemanticDbOptions( @Hidden @Tag(tags.should) @HelpMessage("Generate SemanticDBs") @Name("semanticdb") semanticDb: Option[Boolean] = None, @Hidden @Tag(tags.should) @HelpMessage("SemanticDB target root (default to the compiled classes destination directory)") @Name("semanticdbTargetRoot") @Name("semanticdbTargetroot") semanticDbTargetRoot: Option[String] = None, @Hidden @Tag(tags.should) @HelpMessage("SemanticDB source root (default to the project root directory)") @Name("semanticdbSourceRoot") @Name("semanticdbSourceroot") semanticDbSourceRoot: Option[String] = None ) object SemanticDbOptions { implicit lazy val parser: Parser[SemanticDbOptions] = Parser.derive implicit lazy val help: Help[SemanticDbOptions] = Help.derive implicit lazy val jsonCodec: JsonValueCodec[SemanticDbOptions] = JsonCodecMaker.make } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/SharedBspFileOptions.scala ================================================ package scala.cli.commands.shared import caseapp.* import scala.cli.commands.tags // format: off final case class SharedBspFileOptions( @Group(HelpGroup.BSP.toString) @Name("bspDir") @HelpMessage("Custom BSP configuration location") @Tag(tags.implementation) @Hidden bspDirectory: Option[String] = None, @Group(HelpGroup.BSP.toString) @Name("name") @HelpMessage("Name of BSP") @Hidden @Tag(tags.implementation) bspName: Option[String] = None ) // format: on object SharedBspFileOptions { implicit lazy val parser: Parser[SharedBspFileOptions] = Parser.derive implicit lazy val help: Help[SharedBspFileOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/SharedCompilationServerOptions.scala ================================================ package scala.cli.commands.shared import bloop.rifle.internal.BuildInfo import bloop.rifle.{BloopRifleConfig, BloopVersion, BspConnectionAddress} import caseapp.* import com.github.plokhotnyuk.jsoniter_scala.core.* import com.github.plokhotnyuk.jsoniter_scala.macros.* import coursier.cache.FileCache import coursier.core.Version as Ver import coursier.util.Task import java.io.File import java.nio.file.{AtomicMoveNotSupportedException, FileAlreadyExistsException, Files, Paths} import java.util.Random import scala.build.internal.Util import scala.build.{Bloop, Logger, Os} import scala.cli.commands.Constants import scala.cli.commands.bloop.BloopJson import scala.cli.internal.Pid import scala.concurrent.duration.{Duration, FiniteDuration} import scala.util.Properties // format: off final case class SharedCompilationServerOptions( @Group(HelpGroup.CompilationServer.toString) @HelpMessage("Protocol to use to open a BSP connection with Bloop") @ValueDescription("tcp|local|default") @Hidden bloopBspProtocol: Option[String] = None, @Group(HelpGroup.CompilationServer.toString) @HelpMessage("Socket file to use to open a BSP connection with Bloop") @ValueDescription("path") @Hidden bloopBspSocket: Option[String] = None, @Group(HelpGroup.CompilationServer.toString) @HelpMessage("Host the compilation server should bind to") @ValueDescription("host") @Hidden bloopHost: Option[String] = None, @Group(HelpGroup.CompilationServer.toString) @HelpMessage("Port the compilation server should bind to (pass `-1` to pick a random port)") @ValueDescription("port|-1") @Hidden bloopPort: Option[Int] = None, @Group(HelpGroup.CompilationServer.toString) @HelpMessage("Daemon directory of the Bloop daemon (directory with lock, pid, and socket files)") @ValueDescription("path") @Hidden bloopDaemonDir: Option[String] = None, @Group(HelpGroup.CompilationServer.toString) @HelpMessage("If Bloop isn't already running, the version we should start") @ValueDescription("version") @Hidden bloopVersion: Option[String] = None, @Hidden @Group(HelpGroup.CompilationServer.toString) @HelpMessage("Maximum duration to wait for the BSP connection to be opened") @ValueDescription("duration") bloopBspTimeout: Option[String] = None, @Hidden @Group(HelpGroup.CompilationServer.toString) @HelpMessage("Duration between checks of the BSP connection state") @ValueDescription("duration") bloopBspCheckPeriod: Option[String] = None, @Hidden @Group(HelpGroup.CompilationServer.toString) @HelpMessage("Maximum duration to wait for the compilation server to start up") @ValueDescription("duration") bloopStartupTimeout: Option[String] = None, @Group(HelpGroup.CompilationServer.toString) @HelpMessage("Include default JVM options for Bloop") @Hidden bloopDefaultJavaOpts: Boolean = true, @Group(HelpGroup.CompilationServer.toString) @HelpMessage("Pass java options to use by Bloop server") @Hidden bloopJavaOpt: List[String] = Nil, @Group(HelpGroup.CompilationServer.toString) @HelpMessage("Bloop global options file") @Hidden bloopGlobalOptionsFile: Option[String] = None, @Group(HelpGroup.CompilationServer.toString) @HelpMessage(s"JVM to use to start Bloop (e.g. 'system|${Constants.minimumBloopJavaVersion}', 'temurin:21', …)") @Hidden bloopJvm: Option[String] = None, @Group(HelpGroup.CompilationServer.toString) @HelpMessage("Working directory for Bloop, if it needs to be started") @Hidden bloopWorkingDir: Option[String] = None, @Group(HelpGroup.CompilationServer.toString) @HelpMessage("Enable / disable usage of Bloop compilation server. Bloop is used by default so use `--server=false` to disable it. Disabling compilation server allows to test compilation in more controlled mannter (no caching or incremental compiler) but has a detrimental effect of performance.") server: Option[Boolean] = None ) { // format: on private def pidOrRandom: Either[Int, Int] = Option((new Pid).get()).map(_.intValue()).map(Right(_)).getOrElse { val r = new Random Left(r.nextInt()) } private def socketDirectory(directories: scala.build.Directories): os.Path = { val dir = directories.bspSocketDir // Ensuring that whenever dir exists, it has the right permissions if (!os.isDir(dir)) { val tmpDir = dir / os.up / s".${dir.last}.tmp-${pidOrRandom.merge}" try { os.makeDir.all(tmpDir) if (!Properties.isWin) os.perms.set(tmpDir, "rwx------") try os.move(tmpDir, dir, atomicMove = true) catch { case _: AtomicMoveNotSupportedException => try os.move(tmpDir, dir) catch { case _: FileAlreadyExistsException => } case _: FileAlreadyExistsException => } } finally if (os.exists(tmpDir)) os.remove(tmpDir) } dir } private def bspSocketFile(directories: => scala.build.Directories): File = { val (socket, deleteOnExit) = bloopBspSocket match { case Some(path) => (os.Path(path, Os.pwd), false) case None => val dir = socketDirectory(directories) val fileName = pidOrRandom .map("proc-" + _) .left.map("conn-" + _) .merge val path = dir / fileName if (os.exists(path)) // isFile is false for domain sockets os.remove(path) (path, true) } if (deleteOnExit) Runtime.getRuntime.addShutdownHook( new Thread("delete-bloop-bsp-named-socket") { override def run() = Files.deleteIfExists(socket.toNIO) } ) socket.toIO.getCanonicalFile } def defaultBspSocketOrPort( directories: => scala.build.Directories ): Option[() => BspConnectionAddress] = { def namedSocket = Some(() => BspConnectionAddress.UnixDomainSocket(bspSocketFile(directories))) def default = namedSocket bloopBspProtocol.filter(_ != "default") match { case None => default case Some("tcp") => None case Some("local") => namedSocket case Some(other) => sys.error( s"Invalid bloop BSP protocol value: '$other' (expected 'tcp', 'local', or 'default')" ) } } private def parseDuration(name: String, valueOpt: Option[String]): Option[FiniteDuration] = valueOpt.map(_.trim).filter(_.nonEmpty).map(Duration(_)).map { case d: FiniteDuration => d case d => sys.error(s"Expected finite $name duration, got $d") } def bloopBspTimeoutDuration: Option[FiniteDuration] = parseDuration("BSP connection timeout", bloopBspTimeout) def bloopBspCheckPeriodDuration: Option[FiniteDuration] = parseDuration("BSP connection check period", bloopBspCheckPeriod) def bloopStartupTimeoutDuration: Option[FiniteDuration] = parseDuration("connection server startup timeout", bloopStartupTimeout) def retainedBloopVersion: BloopRifleConfig.BloopVersionConstraint = bloopVersion .map(_.trim) .filter(_.nonEmpty) .fold[BloopRifleConfig.BloopVersionConstraint](BloopRifleConfig.AtLeast( BloopVersion(BuildInfo.version) ))(v => BloopRifleConfig.Strict(BloopVersion(v))) def bloopDefaultJvmOptions(logger: Logger): Option[List[String]] = { val filePathOpt = bloopGlobalOptionsFile.filter(_.trim.nonEmpty).map(os.Path(_, Os.pwd)) for (filePath <- filePathOpt) yield if (os.exists(filePath) && os.isFile(filePath)) try { val content = os.read.bytes(filePath) val bloopJson = readFromArray(content)(using BloopJson.codec) bloopJson.javaOptions } catch { case e: Throwable => logger.message(s"Error parsing global bloop config in '$filePath':") Util.printException(e) List.empty } else { logger.message(s"Bloop global options file '$filePath' not found.") List.empty } } def bloopRifleConfig( logger: Logger, cache: FileCache[Task], verbosity: Int, javaPath: String, directories: => scala.build.Directories, javaV: Option[Int] = None ): BloopRifleConfig = { val portOpt = bloopPort.filter(_ != 0) match { case Some(n) if n < 0 => Some(_root_.bloop.rifle.internal.Util.randomPort()) case other => other } val address = ( bloopHost.filter(_.nonEmpty), portOpt, bloopDaemonDir.filter(_.nonEmpty) ) match { case (_, _, Some(path)) => BloopRifleConfig.Address.DomainSocket(Paths.get(path)) case (None, None, None) => val isBloopMainLine = Ver(retainedBloopVersion.version.raw) < Ver("1.4.12") if (isBloopMainLine) BloopRifleConfig.Address.Tcp( host = BloopRifleConfig.defaultHost, port = BloopRifleConfig.defaultPort ) else BloopRifleConfig.Address.DomainSocket(directories.bloopDaemonDir.toNIO) case (hostOpt, portOpt0, _) => BloopRifleConfig.Address.Tcp( host = hostOpt.getOrElse(BloopRifleConfig.defaultHost), port = portOpt0.getOrElse(BloopRifleConfig.defaultPort) ) } val workingDir = bloopWorkingDir .filter(_.trim.nonEmpty) .map(os.Path(_, Os.pwd)) .getOrElse(directories.bloopWorkingDir) val baseConfig = BloopRifleConfig.default( address, v => Bloop.bloopClassPath(logger, cache, v), workingDir.toIO ) baseConfig.copy( javaPath = javaPath, bspSocketOrPort = defaultBspSocketOrPort(directories), bspStdout = if (verbosity >= 3) Some(System.err) else None, bspStderr = Some(System.err), period = bloopBspCheckPeriodDuration.getOrElse(baseConfig.period), timeout = bloopBspTimeoutDuration.getOrElse(baseConfig.timeout), initTimeout = bloopStartupTimeoutDuration.getOrElse(baseConfig.initTimeout), javaOpts = (if (bloopDefaultJavaOpts) baseConfig.javaOpts else Nil) ++ bloopJavaOpt ++ bloopDefaultJvmOptions(logger).getOrElse(Nil), minimumBloopJvm = javaV.getOrElse(Constants.minimumBloopJavaVersion), retainedBloopVersion = retainedBloopVersion ) } } object SharedCompilationServerOptions { implicit lazy val parser: Parser[SharedCompilationServerOptions] = Parser.derive implicit lazy val help: Help[SharedCompilationServerOptions] = Help.derive implicit lazy val jsonCodec: JsonValueCodec[SharedCompilationServerOptions] = JsonCodecMaker.make } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/SharedDebugOptions.scala ================================================ package scala.cli.commands.shared import caseapp.* import caseapp.core.help.Help import scala.cli.commands.tags // format: off final case class SharedDebugOptions( @Group(HelpGroup.Debug.toString) @HelpMessage("Turn debugging on") @Tag(tags.should) debug: Boolean = false, @Group(HelpGroup.Debug.toString) @HelpMessage("Debug port (5005 by default)") @Tag(tags.should) debugPort: Option[String] = None, @Group(HelpGroup.Debug.toString) @Tag(tags.should) @HelpMessage("Debug mode (attach by default)") @ValueDescription("attach|a|listen|l") debugMode: Option[String] = None ) // format: on object SharedDebugOptions { implicit lazy val parser: Parser[SharedDebugOptions] = Parser.derive implicit lazy val help: Help[SharedDebugOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/SharedDependencyOptions.scala ================================================ package scala.cli.commands.shared import caseapp.* import com.github.plokhotnyuk.jsoniter_scala.core.* import com.github.plokhotnyuk.jsoniter_scala.macros.* import scala.build.preprocessing.directives.Repository import scala.cli.commands.tags // format: off final case class SharedDependencyOptions( @Group(HelpGroup.Dependency.toString) @HelpMessage("Add dependencies") @Tag(tags.must) @Name("dep") dependency: List[String] = Nil, @Group(HelpGroup.Dependency.toString) @HelpMessage("Add compile-only dependencies") @Tag(tags.must) @Name("compileDep") @Name("compileLib") compileOnlyDependency: List[String] = Nil, @Group(HelpGroup.Dependency.toString) @Tag(tags.should) @Tag(tags.inShortHelp) @HelpMessage(Repository.usageMsg) @Name("r") @Name("repo") repository: List[String] = Nil, @Group(HelpGroup.Scala.toString) @Name("P") @Name("plugin") @Tag(tags.must) @HelpMessage("Add compiler plugin dependencies") compilerPlugin: List[String] = Nil ) // format: on object SharedDependencyOptions { implicit lazy val parser: Parser[SharedDependencyOptions] = Parser.derive implicit lazy val help: Help[SharedDependencyOptions] = Help.derive implicit lazy val jsonCodec: JsonValueCodec[SharedDependencyOptions] = JsonCodecMaker.make } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/SharedInputOptions.scala ================================================ package scala.cli.commands.shared import caseapp.* import scala.cli.commands.tags // format: off final case class SharedInputOptions( @Hidden @Tag(tags.implementation) defaultForbiddenDirectories: Boolean = true, @Hidden @Tag(tags.implementation) forbid: List[String] = Nil ) // format: on object SharedInputOptions { implicit lazy val parser: Parser[SharedInputOptions] = Parser.derive implicit lazy val help: Help[SharedInputOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/SharedJavaOptions.scala ================================================ package scala.cli.commands.shared import caseapp.* import scala.cli.commands.tags // format: off final case class SharedJavaOptions( @Group(HelpGroup.Java.toString) @HelpMessage("Set Java options, such as `-Xmx1g`") @ValueDescription("java-options") @Tag(tags.must) @Name("J") javaOpt: List[String] = Nil, @Recurse javaProperties: JavaPropOptions = JavaPropOptions(), ) { // format: on def allJavaOpts: Seq[String] = javaOpt ++ javaProperties.javaProp.filter(_.nonEmpty).map(_.split("=", 2)).map { case Array(k) => s"-D$k" case Array(k, v) => s"-D$k=$v" } } object SharedJavaOptions { implicit lazy val parser: Parser[SharedJavaOptions] = Parser.derive implicit lazy val help: Help[SharedJavaOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/SharedJvmOptions.scala ================================================ package scala.cli.commands.shared import caseapp.* import com.github.plokhotnyuk.jsoniter_scala.core.* import com.github.plokhotnyuk.jsoniter_scala.macros.* import scala.cli.commands.tags // format: off final case class SharedJvmOptions( @Recurse sharedDebug: SharedDebugOptions = SharedDebugOptions(), @Group(HelpGroup.Java.toString) @HelpMessage("Set the Java home directory") @Tag(tags.should) @ValueDescription("path") javaHome: Option[String] = None, @Group(HelpGroup.Java.toString) @HelpMessage("Use a specific JVM, such as `14`, `temurin:11`, or `graalvm:21`, or `system`. " + "scala-cli uses [coursier](https://get-coursier.io/) to fetch JVMs, so you can use `cs java --available` to list the available JVMs.") @ValueDescription("jvm-name") @Tag(tags.should) @Name("j") @Tag(tags.inShortHelp) jvm: Option[String] = None, @Group(HelpGroup.Java.toString) @HelpMessage("JVM index URL") @ValueDescription("url") @Tag(tags.implementation) @Hidden jvmIndex: Option[String] = None, @Group(HelpGroup.Java.toString) @HelpMessage("Operating system to use when looking up in the JVM index") @ValueDescription("linux|linux-musl|darwin|windows|…") @Tag(tags.implementation) @Hidden jvmIndexOs: Option[String] = None, @Group(HelpGroup.Java.toString) @HelpMessage("CPU architecture to use when looking up in the JVM index") @ValueDescription("amd64|arm64|arm|…") @Tag(tags.implementation) @Hidden jvmIndexArch: Option[String] = None, @Group(HelpGroup.Java.toString) @HelpMessage("Javac plugin dependencies or files") @Tag(tags.should) @Hidden javacPlugin: List[String] = Nil, @Group(HelpGroup.Java.toString) @HelpMessage("Javac options") @Name("javacOpt") @Tag(tags.should) @Tag(tags.inShortHelp) @Hidden javacOption: List[String] = Nil, @Group(HelpGroup.Java.toString) @Tag(tags.implementation) @HelpMessage("Port for BSP debugging") @Hidden bspDebugPort: Option[String] = None ) // format: on object SharedJvmOptions { implicit lazy val parser: Parser[SharedJvmOptions] = Parser.derive implicit lazy val help: Help[SharedJvmOptions] = Help.derive implicit lazy val jsonCodec: JsonValueCodec[SharedJvmOptions] = JsonCodecMaker.make } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala ================================================ package scala.cli.commands.shared import bloop.rifle.BloopRifleConfig import caseapp.* import caseapp.core.Arg import caseapp.core.help.Help import com.github.plokhotnyuk.jsoniter_scala.core.* import com.github.plokhotnyuk.jsoniter_scala.macros.* import coursier.cache.FileCache import coursier.util.Task import coursier.version.Version import dependency.AnyDependency import dependency.parser.DependencyParser import java.io.{File, InputStream} import java.util.concurrent.atomic.AtomicBoolean import scala.build.* import scala.build.EitherCps.{either, value} import scala.build.Ops.EitherOptOps import scala.build.compiler.{BloopCompilerMaker, ScalaCompilerMaker, SimpleScalaCompilerMaker} import scala.build.errors.{AmbiguousPlatformError, BuildException, ConfigDbException} import scala.build.input.{Element, Inputs, ResourceDirectory, ScalaCliInvokeData} import scala.build.interactive.Interactive import scala.build.interactive.Interactive.{InteractiveAsk, InteractiveNop} import scala.build.internal.util.WarningMessages import scala.build.internal.{Constants, FetchExternalBinary, OsLibc} import scala.build.internals.ConsoleUtils.ScalaCliConsole import scala.build.options.{BuildOptions, Platform, ShadowingSeq} import scala.build.preprocessing.directives.ClasspathUtils.* import scala.build.preprocessing.directives.Toolkit.maxScalaNativeWarningMsg import scala.build.preprocessing.directives.{Python, Toolkit} import scala.cli.ScalaCli import scala.cli.commands.publish.ConfigUtil.* import scala.cli.commands.tags import scala.cli.commands.util.JvmUtils import scala.cli.commands.util.ScalacOptionsUtil.* import scala.cli.config.Key.BooleanEntry import scala.cli.config.{ConfigDb, Keys} import scala.cli.launcher.PowerOptions import scala.cli.util.ConfigDbUtils import scala.util.Properties // format: off final case class SharedOptions( @Recurse sharedVersionOptions: SharedVersionOptions = SharedVersionOptions(), @Recurse sourceGenerator: SourceGeneratorOptions = SourceGeneratorOptions(), @Recurse suppress: SuppressWarningOptions = SuppressWarningOptions(), @Recurse logging: LoggingOptions = LoggingOptions(), @Recurse powerOptions: PowerOptions = PowerOptions(), @Recurse js: ScalaJsOptions = ScalaJsOptions(), @Recurse native: ScalaNativeOptions = ScalaNativeOptions(), @Recurse compilationServer: SharedCompilationServerOptions = SharedCompilationServerOptions(), @Recurse dependencies: SharedDependencyOptions = SharedDependencyOptions(), @Recurse scalac: ScalacOptions = ScalacOptions(), @Recurse jvm: SharedJvmOptions = SharedJvmOptions(), @Recurse coursier: CoursierOptions = CoursierOptions(), @Recurse workspace: SharedWorkspaceOptions = SharedWorkspaceOptions(), @Recurse sharedPython: SharedPythonOptions = SharedPythonOptions(), @Recurse benchmarking: BenchmarkingOptions = BenchmarkingOptions(), @Group(HelpGroup.Scala.toString) @HelpMessage(s"Set the Scala version (${Constants.defaultScalaVersion} by default)") @ValueDescription("version") @Name("S") @Name("scala") @Tag(tags.must) scalaVersion: Option[String] = None, @Group(HelpGroup.Scala.toString) @HelpMessage("Set the Scala binary version") @ValueDescription("version") @Hidden @Name("B") @Name("scalaBinary") @Name("scalaBin") @Tag(tags.must) scalaBinaryVersion: Option[String] = None, @Recurse scalacExtra: ScalacExtraOptions = ScalacExtraOptions(), @Recurse snippet: SnippetOptions = SnippetOptions(), @Recurse markdown: MarkdownOptions = MarkdownOptions(), @Group(HelpGroup.Java.toString) @HelpMessage("Add extra JARs and compiled classes to the class path") @ValueDescription("paths") @Name("jar") @Name("jars") @Name("extraJar") @Name("class") @Name("extraClass") @Name("classes") @Name("extraClasses") @Name("-classpath") @Name("-cp") @Name("classpath") @Name("classPath") @Name("extraClassPath") @Tag(tags.must) extraJars: List[String] = Nil, @Group(HelpGroup.Java.toString) @HelpMessage("Add extra JARs in the compilaion class path. Mainly using to run code in managed environments like Spark not to include certain depenencies on runtime ClassPath.") @ValueDescription("paths") @Name("compileOnlyJar") @Name("compileOnlyJars") @Name("extraCompileOnlyJar") @Tag(tags.should) extraCompileOnlyJars: List[String] = Nil, @Group(HelpGroup.Java.toString) @HelpMessage("Add extra source JARs") @ValueDescription("paths") @Name("sourceJar") @Name("sourceJars") @Name("extraSourceJar") @Tag(tags.should) extraSourceJars: List[String] = Nil, @Group(HelpGroup.Java.toString) @HelpMessage("Add a resource directory") @ValueDescription("paths") @Name("resourceDir") @Tag(tags.must) resourceDirs: List[String] = Nil, @Hidden @Group(HelpGroup.Java.toString) @HelpMessage("Put project in class paths as a JAR rather than as a byte code directory") @Tag(tags.experimental) asJar: Boolean = false, @Group(HelpGroup.Scala.toString) @HelpMessage("Specify platform") @ValueDescription("scala-js|scala-native|jvm") @Tag(tags.should) @Tag(tags.inShortHelp) platform: Option[String] = None, @Group(HelpGroup.Scala.toString) @Tag(tags.implementation) @Hidden scalaLibrary: Option[Boolean] = None, @Group(HelpGroup.Scala.toString) @HelpMessage("Allows to include the Scala compiler artifacts on the classpath.") @Tag(tags.must) @Name("withScalaCompiler") @Name("-with-compiler") withCompiler: Option[Boolean] = None, @Group(HelpGroup.Java.toString) @HelpMessage("Do not add dependency to Scala Standard library. This is useful, when Scala CLI works with pure Java projects.") @Tag(tags.implementation) @Hidden java: Option[Boolean] = None, @Group(HelpGroup.Scala.toString) @HelpMessage("Should include Scala CLI runner on the runtime ClassPath. Runner is added by default for application running on JVM using standard Scala versions. Runner is used to make stack traces more readable in case of application failure.") @Tag(tags.implementation) @Hidden runner: Option[Boolean] = None, @Recurse semanticDbOptions: SemanticDbOptions = SemanticDbOptions(), @Recurse input: SharedInputOptions = SharedInputOptions(), @Recurse helpGroups: HelpGroupOptions = HelpGroupOptions(), @Hidden strictBloopJsonCheck: Option[Boolean] = None, @Group(HelpGroup.Scala.toString) @Name("d") @Name("output-directory") @Name("destination") @Name("compileOutput") @Name("compileOut") @HelpMessage("Copy compilation results to output directory using either relative or absolute path") @ValueDescription("/example/path") @Tag(tags.must) compilationOutput: Option[String] = None, @Group(HelpGroup.Scala.toString) @HelpMessage(s"Add toolkit to classPath (not supported in Scala 2.12), 'default' version for Scala toolkit: ${Constants.toolkitDefaultVersion}, 'default' version for typelevel toolkit: ${Constants.typelevelToolkitDefaultVersion}") @ValueDescription("version|default") @Name("toolkit") @Tag(tags.implementation) @Tag(tags.inShortHelp) withToolkit: Option[String] = None, @HelpMessage("Exclude sources") exclude: List[String] = Nil, @HelpMessage("Force object wrapper for scripts") @Tag(tags.experimental) objectWrapper: Option[Boolean] = None, @Recurse scope: ScopeOptions = ScopeOptions() ) extends HasGlobalOptions { // format: on def logger: Logger = logging.logger override def global: GlobalOptions = GlobalOptions(logging = logging, globalSuppress = suppress.global, powerOptions = powerOptions) private def scalaJsOptions(opts: ScalaJsOptions): options.ScalaJsOptions = { import opts._ options.ScalaJsOptions( version = jsVersion, mode = options.ScalaJsMode(jsMode), moduleKindStr = jsModuleKind, checkIr = jsCheckIr, emitSourceMaps = jsEmitSourceMaps, sourceMapsDest = jsSourceMapsPath.filter(_.trim.nonEmpty).map(os.Path(_, Os.pwd)), dom = jsDom, header = jsHeader, allowBigIntsForLongs = jsAllowBigIntsForLongs, avoidClasses = jsAvoidClasses, avoidLetsAndConsts = jsAvoidLetsAndConsts, moduleSplitStyleStr = jsModuleSplitStyle, smallModuleForPackage = jsSmallModuleForPackage, esVersionStr = jsEsVersion, noOpt = jsNoOpt, remapEsModuleImportMap = jsEsModuleImportMap.filter(_.trim.nonEmpty).map(os.Path(_, Os.pwd)), jsEmitWasm = jsEmitWasm.getOrElse(false) ) } private def linkerOptions(opts: ScalaJsOptions): options.scalajs.ScalaJsLinkerOptions = { import opts._ options.scalajs.ScalaJsLinkerOptions( linkerPath = jsLinkerPath .filter(_.trim.nonEmpty) .map(os.Path(_, Os.pwd)), scalaJsVersion = jsVersion.map(_.trim).filter(_.nonEmpty), scalaJsCliVersion = jsCliVersion.map(_.trim).filter(_.nonEmpty), javaArgs = jsCliJavaArg, useJvm = jsCliOnJvm.map { case false => Left(FetchExternalBinary.platformSuffix()) case true => Right(()) } ) } private def scalaNativeOptions( opts: ScalaNativeOptions, maxDefaultScalaNativeVersions: List[(String, String)] ): options.ScalaNativeOptions = { import opts._ options.ScalaNativeOptions( version = nativeVersion, modeStr = nativeMode, ltoStr = nativeLto, gcStr = nativeGc, clang = nativeClang, clangpp = nativeClangpp, linkingOptions = nativeLinking, linkingDefaults = nativeLinkingDefaults, compileOptions = nativeCompile, cCompileOptions = nativeCCompile, cppCompileOptions = nativeCppCompile, compileDefaults = nativeCompileDefaults, embedResources = embedResources, buildTargetStr = nativeTarget, multithreading = nativeMultithreading, maxDefaultNativeVersions = maxDefaultScalaNativeVersions ) } lazy val scalacOptionsFromFiles: List[String] = scalac.argsFiles.flatMap(argFile => ArgSplitter.splitToArgs(os.read(os.Path(argFile.file, os.pwd))) ) def scalacOptions: List[String] = scalac.scalacOption ++ scalacOptionsFromFiles def buildOptions( ignoreErrors: Boolean = false, watchOptions: SharedWatchOptions = SharedWatchOptions() ) : Either[BuildException, scala.build.options.BuildOptions] = either { val releaseOpt = scalacOptions.getScalacOption("-release") val targetOpt = scalacOptions.getScalacPrefixOption("-target") jvm.jvm -> (releaseOpt.toSeq ++ targetOpt) match { case (Some(j), compilerTargets) if compilerTargets.exists(_ != j) => val compilerTargetsString = compilerTargets.distinct.mkString(", ") logger.error( s"Warning: different target JVM ($j) and scala compiler target JVM ($compilerTargetsString) were passed." ) case _ => } val parsedPlatform = platform.map(Platform.normalize).flatMap(Platform.parse) val platformOpt = value { (parsedPlatform, js.js, native.native) match { case (Some(p: Platform.JS.type), _, false) => Right(Some(p)) case (Some(p: Platform.Native.type), false, _) => Right(Some(p)) case (Some(p: Platform.JVM.type), false, false) => Right(Some(p)) case (Some(p), _, _) => val jsSeq = if (js.js) Seq(Platform.JS) else Seq.empty val nativeSeq = if (native.native) Seq(Platform.Native) else Seq.empty val platformsSeq = Seq(p) ++ jsSeq ++ nativeSeq Left(new AmbiguousPlatformError(platformsSeq.distinct.map(_.toString))) case (_, true, true) => Left(new AmbiguousPlatformError(Seq(Platform.JS.toString, Platform.Native.toString))) case (_, true, _) => Right(Some(Platform.JS)) case (_, _, true) => Right(Some(Platform.Native)) case _ => Right(None) } } val (assumedSourceJars, extraRegularJarsAndClasspath) = extraJarsAndClassPath.partition(_.hasSourceJarSuffix) if assumedSourceJars.nonEmpty then val assumedSourceJarsString = assumedSourceJars.mkString(", ") logger.message( s"""[${Console.YELLOW}warn${Console.RESET}] Jars with the ${ScalaCliConsole .GRAY}*-sources.jar${Console.RESET} name suffix are assumed to be source jars. |The following jars were assumed to be source jars and will be treated as such: $assumedSourceJarsString""".stripMargin ) val shouldSuppressDeprecatedWarnings = getOptionOrFromConfig( suppress.global.suppressDeprecatedFeatureWarning, Keys.suppressDeprecatedFeatureWarning ) val (resolvedToolkitDependency, toolkitMaxDefaultScalaNativeVersions) = SharedOptions.resolveToolkitDependencyAndScalaNativeVersionReqs( withToolkit, shouldSuppressDeprecatedWarnings.getOrElse(false), logger ) val scalapyMaxDefaultScalaNativeVersions = if sharedPython.python.contains(true) then List(Constants.scalaPyMaxScalaNative -> Python.maxScalaNativeWarningMsg) else Nil val maxDefaultScalaNativeVersions = toolkitMaxDefaultScalaNativeVersions.toList ++ scalapyMaxDefaultScalaNativeVersions val snOpts = scalaNativeOptions(native, maxDefaultScalaNativeVersions) scala.build.options.BuildOptions( sourceGeneratorOptions = scala.build.options.SourceGeneratorOptions( useBuildInfo = sourceGenerator.useBuildInfo, projectVersion = sharedVersionOptions.projectVersion, computeVersion = value { sharedVersionOptions.computeVersion .map(Positioned.commandLine) .map(scala.build.options.ComputeVersion.parse) .sequence } ), suppressWarningOptions = scala.build.options.SuppressWarningOptions( suppressDirectivesInMultipleFilesWarning = getOptionOrFromConfig( suppress.suppressDirectivesInMultipleFilesWarning, Keys.suppressDirectivesInMultipleFilesWarning ), suppressOutdatedDependencyWarning = getOptionOrFromConfig( suppress.suppressOutdatedDependencyWarning, Keys.suppressOutdatedDependenciessWarning ), suppressExperimentalFeatureWarning = getOptionOrFromConfig( suppress.global.suppressExperimentalFeatureWarning, Keys.suppressExperimentalFeatureWarning ), suppressDeprecatedFeatureWarning = shouldSuppressDeprecatedWarnings ), scalaOptions = scala.build.options.ScalaOptions( scalaVersion = scalaVersion .map(_.trim) .filter(_.nonEmpty) .map(scala.build.options.MaybeScalaVersion(_)), scalaBinaryVersion = scalaBinaryVersion.map(_.trim).filter(_.nonEmpty), addScalaLibrary = scalaLibrary.orElse(java.map(!_)), addScalaCompiler = withCompiler, semanticDbOptions = scala.build.options.SemanticDbOptions( generateSemanticDbs = semanticDbOptions.semanticDb, semanticDbTargetRoot = semanticDbOptions.semanticDbTargetRoot.map(os.Path(_, os.pwd)), semanticDbSourceRoot = semanticDbOptions.semanticDbSourceRoot.map(os.Path(_, os.pwd)) ), scalacOptions = scalacOptions .withScalacExtraOptions(scalacExtra) .toScalacOptShadowingSeq .filterNonRedirected .filterNonDeprecated .map(Positioned.commandLine), compilerPlugins = SharedOptions.parseDependencies( dependencies.compilerPlugin.map(Positioned.none), ignoreErrors ), platform = platformOpt.map(o => Positioned(List(Position.CommandLine()), o)) ), scriptOptions = scala.build.options.ScriptOptions( forceObjectWrapper = objectWrapper ), scalaJsOptions = scalaJsOptions(js), scalaNativeOptions = snOpts, javaOptions = value(scala.cli.commands.util.JvmUtils.javaOptions(jvm)), jmhOptions = scala.build.options.JmhOptions( jmhVersion = benchmarking.jmhVersion, enableJmh = benchmarking.jmh, runJmh = benchmarking.jmh ), classPathOptions = scala.build.options.ClassPathOptions( extraClassPath = extraRegularJarsAndClasspath, extraCompileOnlyJars = extraCompileOnlyClassPath, extraSourceJars = extraSourceJars.extractedClassPath ++ assumedSourceJars, extraRepositories = (ScalaCli.launcherOptions.scalaRunner.cliPredefinedRepository ++ dependencies.repository) .map(_.trim) .filter(_.nonEmpty), extraDependencies = extraDependencies(ignoreErrors, resolvedToolkitDependency), extraCompileOnlyDependencies = extraCompileOnlyDependencies(ignoreErrors, resolvedToolkitDependency) ), internal = scala.build.options.InternalOptions( cache = Some(coursierCache), localRepository = LocalRepo.localRepo(Directories.directories.localRepoDir, logger), verbosity = Some(logging.verbosity), strictBloopJsonCheck = strictBloopJsonCheck, interactive = Some(() => interactive), exclude = exclude.map(Positioned.commandLine), offline = coursier.getOffline(logger) ), notForBloopOptions = scala.build.options.PostBuildOptions( scalaJsLinkerOptions = linkerOptions(js), addRunnerDependencyOpt = runner, python = sharedPython.python, pythonSetup = sharedPython.pythonSetup, scalaPyVersion = sharedPython.scalaPyVersion ), useBuildServer = compilationServer.server ).orElse(watchOptions.buildOptions()) } private def resolvedDependencies( deps: List[String], ignoreErrors: Boolean, extraResolvedDependencies: Seq[Positioned[AnyDependency]] ) = ShadowingSeq.from { SharedOptions.parseDependencies(deps.map(Positioned.none), ignoreErrors) ++ extraResolvedDependencies } private def extraCompileOnlyDependencies( ignoreErrors: Boolean, resolvedDeps: Seq[Positioned[AnyDependency]] ) = resolvedDependencies(dependencies.compileOnlyDependency, ignoreErrors, resolvedDeps) private def extraDependencies( ignoreErrors: Boolean, resolvedDeps: Seq[Positioned[AnyDependency]] ) = resolvedDependencies(dependencies.dependency, ignoreErrors, resolvedDeps) extension (rawClassPath: List[String]) { def extractedClassPath: List[os.Path] = rawClassPath .flatMap(_.split(File.pathSeparator).toSeq) .filter(_.nonEmpty) .distinct .map(os.Path(_, os.pwd)) .flatMap { case cp if os.isDir(cp) => val jarsInTheDirectory = os.walk(cp) .filter(p => os.isFile(p) && p.last.endsWith(".jar")) List(cp) ++ jarsInTheDirectory // .jar paths have to be passed directly, unlike .class case cp => List(cp) } } def extraJarsAndClassPath: List[os.Path] = (extraJars ++ scalacOptions.getScalacOption("-classpath") ++ scalacOptions.getScalacOption( "-cp" )) .extractedClassPath def extraClasspathWasPassed: Boolean = extraJarsAndClassPath.exists(!_.hasSourceJarSuffix) || dependencies.dependency.nonEmpty def extraCompileOnlyClassPath: List[os.Path] = extraCompileOnlyJars.extractedClassPath def globalInteractiveWasSuggested: Either[BuildException, Option[Boolean]] = either { value(ConfigDbUtils.configDb).get(Keys.globalInteractiveWasSuggested) match { case Right(opt) => opt case Left(ex) => logger.debug(ConfigDbException(ex)) None } } def interactive: Either[BuildException, Interactive] = either { ( logging.verbosityOptions.interactive, value(ConfigDbUtils.configDb).get(Keys.interactive) match { case Right(opt) => opt case Left(ex) => logger.debug(ConfigDbException(ex)) None }, value(globalInteractiveWasSuggested) ) match { case (Some(true), _, Some(true)) => InteractiveAsk case (_, Some(true), _) => InteractiveAsk case (Some(true), _, _) => val answers @ List(yesAnswer, _) = List("Yes", "No") InteractiveAsk.chooseOne( s"""You have run the current ${ScalaCli.baseRunnerName} command with the --interactive mode turned on. |Would you like to leave it on permanently?""".stripMargin, answers ) match { case Some(answer) if answer == yesAnswer => val configDb0 = value(ConfigDbUtils.configDb) value { configDb0 .set(Keys.interactive, true) .set(Keys.globalInteractiveWasSuggested, true) .save(Directories.directories.dbPath.toNIO) .wrapConfigException } logger.message( s"--interactive is now set permanently. All future ${ScalaCli.baseRunnerName} commands will run with the flag set to true." ) logger.message( s"If you want to turn this setting off at any point, just run `${ScalaCli.baseRunnerName} config interactive false`." ) case _ => val configDb0 = value(ConfigDbUtils.configDb) value { configDb0 .set(Keys.globalInteractiveWasSuggested, true) .save(Directories.directories.dbPath.toNIO) .wrapConfigException } logger.message( s"If you want to turn this setting permanently on at any point, just run `${ScalaCli.baseRunnerName} config interactive true`." ) } InteractiveAsk case _ => InteractiveNop } } def getOptionOrFromConfig(cliOption: Option[Boolean], configDbKey: BooleanEntry) = cliOption.orElse( ConfigDbUtils.configDb.map(_.get(configDbKey)) .map { case Right(opt) => opt case Left(ex) => logger.debug(ConfigDbException(ex)) None } .getOrElse(None) ) def bloopRifleConfig(extraBuildOptions: Option[scala.build.options.BuildOptions] = None) : Either[BuildException, BloopRifleConfig] = either { val options = extraBuildOptions.foldLeft(value(buildOptions()))(_.orElse(_)) lazy val defaultJvmHome = value { JvmUtils.downloadJvm(OsLibc.defaultJvm(OsLibc.jvmIndexOs), options) } val javaHomeInfo = compilationServer.bloopJvm .map(jvmId => value(JvmUtils.downloadJvm(jvmId, options))) .orElse { for (javaHome <- options.javaHomeLocationOpt()) yield { val (javaHomeVersion, javaHomeCmd) = OsLibc.javaHomeVersion(javaHome.value) if (javaHomeVersion >= Constants.minimumBloopJavaVersion) scala.build.options.BuildOptions.JavaHomeInfo( javaHome.value, javaHomeCmd, javaHomeVersion ) else defaultJvmHome } }.getOrElse(defaultJvmHome) compilationServer.bloopRifleConfig( logging.logger, coursierCache, logging.verbosity, javaHomeInfo.javaCommand, Directories.directories, Some(javaHomeInfo.version) ) } def compilerMaker( threads: BuildThreads, scaladoc: Boolean = false ): ScalaCompilerMaker = if (scaladoc) SimpleScalaCompilerMaker("java", Nil, scaladoc = true) else if (compilationServer.server.getOrElse(true)) new BloopCompilerMaker( options => bloopRifleConfig(Some(options)), threads.bloop, strictBloopJsonCheckOrDefault, coursier.getOffline(logger).getOrElse(false) ) else SimpleScalaCompilerMaker("java", Nil) lazy val coursierCache: FileCache[Task] = coursier.coursierCache(logging.logger) def inputs( args: Seq[String], defaultInputs: () => Option[Inputs] = () => Inputs.default() )(using ScalaCliInvokeData): Either[BuildException, Inputs] = SharedOptions.inputs( args, defaultInputs, resourceDirs, Directories.directories, logger = logger, coursierCache, workspace.forcedWorkspaceOpt, input.defaultForbiddenDirectories, input.forbid, scriptSnippetList = allScriptSnippets, scalaSnippetList = allScalaSnippets, javaSnippetList = allJavaSnippets, markdownSnippetList = allMarkdownSnippets, enableMarkdown = markdown.enableMarkdown, extraClasspathWasPassed = extraClasspathWasPassed ) def allScriptSnippets: List[String] = snippet.scriptSnippet ++ snippet.executeScript def allScalaSnippets: List[String] = snippet.scalaSnippet ++ snippet.executeScala def allJavaSnippets: List[String] = snippet.javaSnippet ++ snippet.executeJava def allMarkdownSnippets: List[String] = snippet.markdownSnippet ++ snippet.executeMarkdown def hasSnippets = allScriptSnippets.nonEmpty || allScalaSnippets.nonEmpty || allJavaSnippets .nonEmpty || allMarkdownSnippets.nonEmpty def validateInputArgs( args: Seq[String] )(using ScalaCliInvokeData): Seq[Either[String, Seq[Element]]] = Inputs.validateArgs( args, Os.pwd, BuildOptions.Download.changing(coursierCache), SharedOptions.readStdin(logger = logger), !Properties.isWin, enableMarkdown = true ) def strictBloopJsonCheckOrDefault: Boolean = strictBloopJsonCheck.getOrElse(scala.build.options.InternalOptions.defaultStrictBloopJsonCheck) } object SharedOptions { implicit lazy val parser: Parser[SharedOptions] = Parser.derive implicit lazy val help: Help[SharedOptions] = Help.derive implicit lazy val jsonCodec: JsonValueCodec[SharedOptions] = JsonCodecMaker.make /** [[Inputs]] builder, handy when you don't have a [[SharedOptions]] instance at hand */ def inputs( args: Seq[String], defaultInputs: () => Option[Inputs], resourceDirs: Seq[String], directories: scala.build.Directories, logger: scala.build.Logger, cache: FileCache[Task], forcedWorkspaceOpt: Option[os.Path], defaultForbiddenDirectories: Boolean, forbid: List[String], scriptSnippetList: List[String], scalaSnippetList: List[String], javaSnippetList: List[String], markdownSnippetList: List[String], enableMarkdown: Boolean = false, extraClasspathWasPassed: Boolean = false )(using ScalaCliInvokeData): Either[BuildException, Inputs] = { val resourceInputs = resourceDirs .map(os.Path(_, Os.pwd)) .map { path => if (!os.exists(path)) logger.message(s"WARNING: provided resource directory path doesn't exist: $path") path } .map(ResourceDirectory.apply) val maybeInputs = Inputs( args, Os.pwd, defaultInputs = defaultInputs, download = BuildOptions.Download.changing(cache), stdinOpt = readStdin(logger = logger), scriptSnippetList = scriptSnippetList, scalaSnippetList = scalaSnippetList, javaSnippetList = javaSnippetList, markdownSnippetList = markdownSnippetList, acceptFds = !Properties.isWin, forcedWorkspace = forcedWorkspaceOpt, enableMarkdown = enableMarkdown, allowRestrictedFeatures = ScalaCli.allowRestrictedFeatures, extraClasspathWasPassed = extraClasspathWasPassed ) maybeInputs.map { inputs => val forbiddenDirs = (if (defaultForbiddenDirectories) myDefaultForbiddenDirectories else Nil) ++ forbid.filter(_.trim.nonEmpty).map(os.Path(_, Os.pwd)) inputs .add(resourceInputs) .checkAttributes(directories) .avoid(forbiddenDirs, directories) } } private def readStdin(in: InputStream = System.in, logger: Logger): Option[Array[Byte]] = if (in == null) { logger.debug("No stdin available") None } else { logger.debug("Reading stdin") val result = in.readAllBytes() logger.debug(s"Done reading stdin (${result.length} B)") Some(result) } private def myDefaultForbiddenDirectories: Seq[os.Path] = if (Properties.isWin) Seq(os.Path("""C:\Windows\System32""")) else Nil def parseDependencies( deps: List[Positioned[String]], ignoreErrors: Boolean ): Seq[Positioned[AnyDependency]] = deps.map(_.map(_.trim)).filter(_.value.nonEmpty) .flatMap { posDepStr => val depStr = posDepStr.value DependencyParser.parse(depStr) match { case Left(err) => if (ignoreErrors) Nil else sys.error(s"Error parsing dependency '$depStr': $err") case Right(dep) => Seq(posDepStr.map(_ => dep)) } } // TODO: remove this state after resolving https://github.com/VirtusLab/scala-cli/issues/2658 private val loggedDeprecatedToolkitWarning: AtomicBoolean = AtomicBoolean(false) private def resolveToolkitDependencyAndScalaNativeVersionReqs( toolkitVersion: Option[String], shouldSuppressDeprecatedWarnings: Boolean, logger: Logger ): (Seq[Positioned[AnyDependency]], Seq[(String, String)]) = { if ( !shouldSuppressDeprecatedWarnings && (toolkitVersion.contains("latest") || toolkitVersion.contains(Toolkit.typelevel + ":latest") || toolkitVersion.contains( Constants.typelevelOrganization + ":latest" )) && !loggedDeprecatedToolkitWarning.getAndSet(true) ) logger.message( WarningMessages.deprecatedToolkitLatest( s"--toolkit ${toolkitVersion.map(_.replace("latest", "default")).getOrElse("default")}" ) ) val (dependencies, toolkitDefinitions) = toolkitVersion.toList.map(Positioned.commandLine) .flatMap(Toolkit.resolveDependenciesWithRequirements(_).map((wbr, td) => wbr.value -> td)) .unzip val maxScalaNativeVersions = toolkitDefinitions.flatMap { case Toolkit.ToolkitDefinitions( isScalaToolkitDefault, explicitScalaToolkitVersion, isTypelevelToolkitDefault, _ ) => val st = if (isScalaToolkitDefault) Seq(Constants.toolkitMaxScalaNative -> Toolkit.maxScalaNativeWarningMsg( toolkitName = "Scala Toolkit", toolkitVersion = Constants.toolkitDefaultVersion, maxNative = Constants.toolkitMaxScalaNative )) else explicitScalaToolkitVersion.toList .map(Version(_)) .filter(_ <= Version(Constants.toolkitVersionForNative04)) .flatMap(v => List(Constants.scalaNativeVersion04 -> maxScalaNativeWarningMsg( toolkitName = "Scala Toolkit", toolkitVersion = v.toString(), Constants.scalaNativeVersion04 )) ) val tlt = if (isTypelevelToolkitDefault) Seq(Constants.typelevelToolkitMaxScalaNative -> Toolkit.maxScalaNativeWarningMsg( toolkitName = "TypeLevel Toolkit", toolkitVersion = Constants.typelevelToolkitDefaultVersion, maxNative = Constants.typelevelToolkitMaxScalaNative )) else Nil st ++ tlt } dependencies -> maxScalaNativeVersions } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/SharedPythonOptions.scala ================================================ package scala.cli.commands.shared import caseapp.* import scala.cli.commands.{Constants, tags} // format: off final case class SharedPythonOptions( @Tag(tags.experimental) @HelpMessage("Set Java options so that Python can be loaded") pythonSetup: Option[Boolean] = None, @Tag(tags.experimental) @HelpMessage("Enable Python support via ScalaPy") @ExtraName("py") python: Option[Boolean] = None, @Tag(tags.experimental) @HelpMessage(s"Set ScalaPy version (${Constants.scalaPyVersion} by default)") @ExtraName("scalapyVersion") scalaPyVersion: Option[String] = None ) // format: on object SharedPythonOptions { implicit lazy val parser: Parser[SharedPythonOptions] = Parser.derive implicit lazy val help: Help[SharedPythonOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/SharedVersionOptions.scala ================================================ package scala.cli.commands.shared import caseapp.* import scala.cli.commands.tags // format: off final case class SharedVersionOptions( @Group(HelpGroup.ProjectVersion.toString) @HelpMessage("Method used to compute the project version") @ValueDescription("git|git:tag|command:...") @Tag(tags.restricted) @Tag(tags.inShortHelp) computeVersion: Option[String] = None, @Group(HelpGroup.ProjectVersion.toString) @HelpMessage("Set the project version") @Tag(tags.restricted) @Tag(tags.inShortHelp) projectVersion: Option[String] = None ) // format: on object SharedVersionOptions { implicit lazy val parser: Parser[SharedVersionOptions] = Parser.derive implicit lazy val help: Help[SharedVersionOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/SharedWatchOptions.scala ================================================ package scala.cli.commands.shared import caseapp.* import scala.build.options.{BuildOptions, WatchOptions} import scala.cli.commands.tags // format: off final case class SharedWatchOptions( @Group(HelpGroup.Watch.toString) @HelpMessage("Run the application in the background, automatically wake the thread and re-run if sources have been changed") @Tag(tags.should) @Tag(tags.inShortHelp) @Name("w") watch: Boolean = false, @Group(HelpGroup.Watch.toString) @HelpMessage("Run the application in the background, automatically kill the process and restart if sources have been changed") @Tag(tags.should) @Tag(tags.inShortHelp) @Name("revolver") restart: Boolean = false, @Group(HelpGroup.Watch.toString) @HelpMessage("Watch additional paths for changes (used together with --watch or --restart)") @Tag(tags.experimental) @Name("watchingPath") watching: List[String] = Nil ) { // format: on lazy val watchMode: Boolean = watch || restart def buildOptions(cwd: os.Path = os.pwd): BuildOptions = BuildOptions( watchOptions = WatchOptions( extraWatchPaths = watching.map(os.Path(_, cwd)) ) ) } object SharedWatchOptions { implicit lazy val parser: Parser[SharedWatchOptions] = Parser.derive implicit lazy val help: Help[SharedWatchOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/SharedWorkspaceOptions.scala ================================================ package scala.cli.commands.shared import caseapp.* import com.github.plokhotnyuk.jsoniter_scala.core.* import com.github.plokhotnyuk.jsoniter_scala.macros.* import scala.build.Os import scala.cli.commands.tags // format: off final case class SharedWorkspaceOptions( @Hidden @HelpMessage("Directory where .scala-build is written") @ValueDescription("path") @Tag(tags.implementation) workspace: Option[String] = None ) { // format: on def forcedWorkspaceOpt: Option[os.Path] = workspace .filter(_.trim.nonEmpty) .map(os.Path(_, Os.pwd)) } object SharedWorkspaceOptions { implicit lazy val parser: Parser[SharedWorkspaceOptions] = Parser.derive implicit lazy val help: Help[SharedWorkspaceOptions] = Help.derive implicit lazy val jsonCodec: JsonValueCodec[SharedWorkspaceOptions] = JsonCodecMaker.make } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/SnippetOptions.scala ================================================ package scala.cli.commands.shared import caseapp.* import scala.cli.commands.tags // format: off final case class SnippetOptions( @Group(HelpGroup.Scala.toString) @HelpMessage("Allows to execute a passed string as a Scala script") @Tag(tags.should) scriptSnippet: List[String] = List.empty, @Group(HelpGroup.Scala.toString) @HelpMessage("A synonym to --script-snippet, which defaults the sub-command to `run` when no sub-command is passed explicitly") @Hidden @Name("e") @Name("executeScalaScript") @Name("executeSc") @Tag(tags.should) executeScript: List[String] = List.empty, @Group(HelpGroup.Scala.toString) @HelpMessage("Allows to execute a passed string as Scala code") @Tag(tags.should) scalaSnippet: List[String] = List.empty, @Group(HelpGroup.Scala.toString) @HelpMessage("A synonym to --scala-snippet, which defaults the sub-command to `run` when no sub-command is passed explicitly") @Hidden @Tag(tags.implementation) executeScala: List[String] = List.empty, @Group(HelpGroup.Java.toString) @HelpMessage("Allows to execute a passed string as Java code") @Tag(tags.implementation) javaSnippet: List[String] = List.empty, @Group(HelpGroup.Java.toString) @Tag(tags.implementation) @HelpMessage("A synonym to --scala-snippet, which defaults the sub-command to `run` when no sub-command is passed explicitly") executeJava: List[String] = List.empty, @Group(HelpGroup.Markdown.toString) @HelpMessage("Allows to execute a passed string as Markdown code") @Name("mdSnippet") @Tag(tags.experimental) markdownSnippet: List[String] = List.empty, @Group(HelpGroup.Markdown.toString) @HelpMessage("A synonym to --markdown-snippet, which defaults the sub-command to `run` when no sub-command is passed explicitly") @Name("executeMd") @Tag(tags.experimental) @Hidden executeMarkdown: List[String] = List.empty, ) // format: on object SnippetOptions { implicit lazy val parser: Parser[SnippetOptions] = Parser.derive implicit lazy val help: Help[SnippetOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/SourceGeneratorOptions.scala ================================================ package scala.cli.commands.shared import caseapp.* import scala.cli.commands.tags // format: off final case class SourceGeneratorOptions( @Group(HelpGroup.SourceGenerator.toString) @Tag(tags.restricted) @HelpMessage("Generate BuildInfo for project") @Name("buildInfo") useBuildInfo: Option[Boolean] = None ) // format: on object SourceGeneratorOptions { implicit lazy val parser: Parser[SourceGeneratorOptions] = Parser.derive implicit lazy val help: Help[SourceGeneratorOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/SuppressWarningOptions.scala ================================================ package scala.cli.commands.shared import caseapp.* import scala.cli.commands.tags // format: off final case class SuppressWarningOptions( @Group(HelpGroup.SuppressWarnings.toString) @Tag(tags.implementation) @HelpMessage("Suppress warnings about using directives in multiple files") @Name("suppressWarningDirectivesInMultipleFiles") suppressDirectivesInMultipleFilesWarning: Option[Boolean] = None, @Group(HelpGroup.SuppressWarnings.toString) @Tag(tags.implementation) @HelpMessage("Suppress warnings about outdated dependencies in project") suppressOutdatedDependencyWarning: Option[Boolean] = None, @Recurse global: GlobalSuppressWarningOptions = GlobalSuppressWarningOptions() ) // format: on object SuppressWarningOptions { implicit lazy val parser: Parser[SuppressWarningOptions] = Parser.derive implicit lazy val help: Help[SuppressWarningOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shared/VerbosityOptions.scala ================================================ package scala.cli.commands.shared import caseapp.* import com.github.plokhotnyuk.jsoniter_scala.core.* import com.github.plokhotnyuk.jsoniter_scala.macros.* import scala.build.interactive.Interactive.* import scala.cli.commands.tags // format: off final case class VerbosityOptions( @Group(HelpGroup.Logging.toString) @HelpMessage("Increase verbosity (can be specified multiple times)") @Tag(tags.implementation) @Name("v") @Name("-verbose") verbose: Int @@ Counter = Tag.of(0), @Group(HelpGroup.Logging.toString) @HelpMessage("Interactive mode") @Name("i") @Tag(tags.implementation) interactive: Option[Boolean] = None, @Group(HelpGroup.Logging.toString) @HelpMessage("Enable actionable diagnostics") @Tag(tags.implementation) actions: Option[Boolean] = None ) { // format: on lazy val verbosity = Tag.unwrap(verbose) def interactiveInstance(forceEnable: Boolean = false) = if (interactive.getOrElse(forceEnable)) InteractiveAsk else InteractiveNop } object VerbosityOptions { implicit lazy val parser: Parser[VerbosityOptions] = Parser.derive implicit lazy val help: Help[VerbosityOptions] = Help.derive implicit val rwCounter: JsonValueCodec[Int @@ Counter] = new JsonValueCodec[Int @@ Counter] { private val intCodec: JsonValueCodec[Int] = JsonCodecMaker.make def decodeValue(in: JsonReader, default: Int @@ Counter) = Tag.of(intCodec.decodeValue(in, Tag.unwrap(default))) def encodeValue(x: Int @@ Counter, out: JsonWriter): Unit = intCodec.encodeValue(Tag.unwrap(x), out) def nullValue: Int @@ Counter = Tag.of(0) } implicit lazy val jsonCodec: JsonValueCodec[VerbosityOptions] = JsonCodecMaker.make } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shebang/Shebang.scala ================================================ package scala.cli.commands.shebang import caseapp.RemainingArgs import caseapp.core.help.HelpFormat import scala.build.Logger import scala.build.input.{ScalaCliInvokeData, SubCommand} import scala.cli.commands.run.Run import scala.cli.commands.shared.SharedOptions import scala.cli.commands.{ScalaCommand, SpecificationLevel} import scala.cli.util.ArgHelpers.* object Shebang extends ScalaCommand[ShebangOptions] { override def stopAtFirstUnrecognized: Boolean = true override def scalaSpecificationLevel: SpecificationLevel = SpecificationLevel.MUST override def helpFormat: HelpFormat = super.helpFormat.withPrimaryGroups(Run.primaryHelpGroups) override def sharedOptions(options: ShebangOptions): Option[SharedOptions] = Run.sharedOptions(options.runOptions) override def invokeData: ScalaCliInvokeData = super.invokeData.copy(subCommand = SubCommand.Shebang) override def runCommand(options: ShebangOptions, args: RemainingArgs, logger: Logger): Unit = Run.runCommand( options.runOptions, args.remaining.headOption.toSeq, args.remaining.drop(1), () => None, logger, invokeData ) } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/shebang/ShebangOptions.scala ================================================ package scala.cli.commands.shebang import caseapp.* import scala.build.internals.ConsoleUtils.ScalaCliConsole import scala.cli.ScalaCli.{baseRunnerName, fullRunnerName, progName} import scala.cli.commands.run.RunOptions import scala.cli.commands.shared.{HasSharedOptions, HelpMessages, SharedOptions} @HelpMessage(ShebangOptions.helpMessage, "", ShebangOptions.detailedHelpMessage) final case class ShebangOptions( @Recurse runOptions: RunOptions = RunOptions() ) extends HasSharedOptions { override def shared: SharedOptions = runOptions.shared } object ShebangOptions { implicit lazy val parser: Parser[ShebangOptions] = Parser.derive implicit lazy val help: Help[ShebangOptions] = Help.derive val cmdName = "shebang" private val helpHeader = "Like `run`, but handier for shebang scripts." val helpMessage: String = HelpMessages.shortHelpMessage(cmdName, helpHeader) val detailedHelpMessage: String = s"""$helpHeader | |This command is equivalent to the `run` sub-command, but it changes the way |$fullRunnerName parses its command-line arguments in order to be compatible |with shebang scripts. | |When relying on the `run` sub-command, inputs and $baseRunnerName options can be mixed, |while program args have to be specified after `--` | ${Console .BOLD}$progName [command] [${baseRunnerName}_options | input]... -- [program_arguments]...${Console .RESET} | |However, for the `shebang` sub-command, only a single input file can be set, while all $baseRunnerName options |have to be set before the input file. |All inputs after the first are treated as program arguments, without the need for `--` | ${Console .BOLD}$progName shebang [${baseRunnerName}_options]... input [program_arguments]...${Console .RESET} | |Using this, it is possible to conveniently set up Unix shebang scripts. For example: | ${ScalaCliConsole.GRAY}#!/usr/bin/env -S $progName shebang --scala-version 2.13 | println("Hello, world")${Console.RESET} | |${HelpMessages.commandDocWebsiteReference(cmdName)}""".stripMargin } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/test/Test.scala ================================================ package scala.cli.commands.test import caseapp.* import caseapp.core.help.HelpFormat import java.nio.file.Path import scala.build.* import scala.build.EitherCps.{either, value} import scala.build.Ops.* import scala.build.errors.{BuildException, CompositeBuildException} import scala.build.internal.{Constants, Runner} import scala.build.internals.ConsoleUtils.ScalaCliConsole import scala.build.options.{BuildOptions, JavaOpt, Platform, Scope} import scala.build.testrunner.{AsmTestRunner, Logger as TestRunnerLogger} import scala.cli.CurrentParams import scala.cli.commands.run.Run import scala.cli.commands.setupide.SetupIde import scala.cli.commands.shared.{HelpCommandGroup, HelpGroup, SharedOptions} import scala.cli.commands.update.Update import scala.cli.commands.{CommandUtils, ScalaCommand, SpecificationLevel, WatchUtil} import scala.cli.config.Keys import scala.cli.packaging.Library.fullClassPathMaybeAsJar import scala.cli.util.ArgHelpers.* import scala.cli.util.ConfigDbUtils object Test extends ScalaCommand[TestOptions] { override def group: String = HelpCommandGroup.Main.toString override def sharedOptions(options: TestOptions): Option[SharedOptions] = Some(options.shared) override def scalaSpecificationLevel: SpecificationLevel = SpecificationLevel.SHOULD override def helpFormat: HelpFormat = super.helpFormat.withPrimaryGroups(Seq(HelpGroup.Test, HelpGroup.Watch)) private def gray = ScalaCliConsole.GRAY private def reset = Console.RESET override def buildOptions(opts: TestOptions): Option[BuildOptions] = Some { import opts.* val baseOptions = shared.buildOptions(watchOptions = watch).orExit(opts.shared.logger) baseOptions.copy( javaOptions = baseOptions.javaOptions.copy( javaOpts = baseOptions.javaOptions.javaOpts ++ sharedJava.allJavaOpts.map(JavaOpt(_)).map(Positioned.commandLine) ), testOptions = baseOptions.testOptions.copy( frameworks = testFrameworks.map(_.trim).filter(_.nonEmpty).map(Positioned.commandLine), testOnly = testOnly.map(_.trim).filter(_.nonEmpty) ), internalDependencies = baseOptions.internalDependencies.copy( addTestRunnerDependencyOpt = Some(true) ) ) } override def runCommand(options: TestOptions, args: RemainingArgs, logger: Logger): Unit = { val initialBuildOptions = buildOptionsOrExit(options) val inputs = options.shared.inputs(args.remaining).orExit(logger) CurrentParams.workspaceOpt = Some(inputs.workspace) SetupIde.runSafe( options.shared, inputs, logger, initialBuildOptions, Some(name), args.remaining ) if (CommandUtils.shouldCheckUpdate) Update.checkUpdateSafe(logger) val threads = BuildThreads.create() val compilerMaker = options.shared.compilerMaker(threads) val cross = options.compileCross.cross.getOrElse(false) val configDb = ConfigDbUtils.configDb.orExit(logger) val actionableDiagnostics = options.shared.logging.verbosityOptions.actions.orElse( configDb.get(Keys.actions).getOrElse(None) ) /** Runs the tests via [[testOnce]] if build was successful * @param builds * build results, checked for failures * @param allowExit * false in watchMode */ def maybeTest(builds: Builds, allowExit: Boolean): Unit = if (builds.anyFailed) { System.err.println("Compilation failed") if (allowExit) sys.exit(1) } else { val optionsKeys = builds.map.keys.toVector.map(_.optionsKey).distinct val builds0 = optionsKeys.flatMap { optionsKey => builds.map.get(CrossKey(optionsKey, Scope.Test)) } val buildsLen = builds0.length val printBeforeAfterMessages = buildsLen > 1 && options.shared.logging.verbosity >= 0 val results = for ((s, idx) <- builds0.zipWithIndex) yield { if (printBeforeAfterMessages) { val scalaStr = s.crossKey.scalaVersion.versionOpt.fold("")(v => s" for Scala $v") val platformStr = s.crossKey.platform.fold("")(p => s", ${p.repr}") System.err.println( s"${gray}Running tests$scalaStr$platformStr$reset" ) System.err.println() } val retCodeOrError = testOnce( s, options.requireTests, args.unparsed, logger, allowExecve = allowExit && buildsLen <= 1, asJar = options.shared.asJar ) if (printBeforeAfterMessages && idx < buildsLen - 1) System.err.println() retCodeOrError } val maybeRetCodes = results.sequence .left.map(CompositeBuildException(_)) val retCodesOpt = if (allowExit) Some(maybeRetCodes.orExit(logger)) else maybeRetCodes.orReport(logger) for (retCodes <- retCodesOpt if !retCodes.forall(_ == 0)) if (allowExit) sys.exit(retCodes.find(_ != 0).getOrElse(1)) else { val red = Console.RED val lightRed = "\u001b[91m" val reset = Console.RESET System.err.println( s"${red}Tests exited with return code $lightRed${retCodes.mkString(", ")}$red.$reset" ) } } if (options.watch.watchMode) { val watcher = Build.watch( inputs, initialBuildOptions, compilerMaker, None, logger, crossBuilds = cross, buildTests = true, partial = None, actionableDiagnostics = actionableDiagnostics, postAction = () => WatchUtil.printWatchMessage() ) { res => for (builds <- res.orReport(logger)) maybeTest(builds, allowExit = false) } try WatchUtil.waitForCtrlC(() => watcher.schedule()) finally watcher.dispose() } else { val builds = Build.build( inputs, initialBuildOptions, compilerMaker, None, logger, crossBuilds = cross, buildTests = true, partial = None, actionableDiagnostics = actionableDiagnostics ) .orExit(logger) maybeTest(builds, allowExit = true) } } private def testOnce( build: Build.Successful, requireTests: Boolean, args: Seq[String], logger: Logger, asJar: Boolean, allowExecve: Boolean ): Either[BuildException, Int] = either { val predefinedTestFrameworks = build.options.testOptions.frameworks build.options.platform.value match { case Platform.JS => val linkerConfig = build.options.scalaJsOptions.linkerConfig(logger) val esModule = build.options.scalaJsOptions.moduleKindStr.exists(m => m == "es" || m == "esmodule") value { Run.withLinkedJs( Seq(build), None, addTestInitializer = true, linkerConfig, value(build.options.scalaJsOptions.fullOpt), build.options.scalaJsOptions.noOpt.getOrElse(false), logger, esModule ) { js => Runner.testJs( build.fullClassPath.map(_.toNIO), js.toIO, requireTests, args, predefinedTestFrameworks.map(_.value), logger, build.options.scalaJsOptions.dom.getOrElse(false), esModule ) }.flatten } case Platform.Native => value { Run.withNativeLauncher( Seq(build), "scala.scalanative.testinterface.TestMain", logger ) { launcher => Runner.testNative( build.fullClassPath.map(_.toNIO), launcher.toIO, predefinedTestFrameworks.map(_.value), requireTests, args, logger ) }.flatten } case Platform.JVM => val classPath = build.fullClassPathMaybeAsJar(asJar) val predefinedTestFrameworks0 = predefinedTestFrameworks match { case f if f.nonEmpty => f case Nil => findTestFramework(classPath.map(_.toNIO), logger).map(Positioned.none).toList } val testOnly = build.options.testOptions.testOnly val extraArgs = (if requireTests then Seq("--require-tests") else Nil) ++ build.options.internal.verbosity.map(v => s"--verbosity=$v") ++ predefinedTestFrameworks0.map(_.value).map(fw => s"--test-framework=$fw") ++ testOnly.map(to => s"--test-only=$to").toSeq ++ Seq("--") ++ args val testRunnerMainClass = if build.artifacts.hasJavaTestRunner then Constants.javaTestRunnerMainClass else Constants.testRunnerMainClass Runner.runJvm( build.options.javaHome().value.javaCommand, build.options.javaOptions.javaOpts.toSeq.map(_.value.value), classPath, testRunnerMainClass, extraArgs, logger, allowExecve = allowExecve ).waitFor() } } private def findTestFramework(classPath: Seq[Path], logger: Logger): Option[String] = { val classPath0 = classPath.map(_.toString) // https://github.com/VirtusLab/scala-cli/issues/426 if classPath0.exists(_.contains("zio-test")) && !classPath0.exists(_.contains("zio-test-sbt")) then { val parentInspector = new AsmTestRunner.ParentInspector(classPath, TestRunnerLogger(logger.verbosity)) Runner.frameworkNames(classPath, parentInspector, logger) match { case Right(f) => f.headOption case Left(_) => logger.message( s"zio-test found in the class path, zio-test-sbt should be added to run zio tests with $fullRunnerName." ) None } } else None } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/test/TestOptions.scala ================================================ package scala.cli.commands.test import caseapp.* import caseapp.core.help.Help import scala.cli.commands.shared.* import scala.cli.commands.tags @HelpMessage(TestOptions.helpMessage, "", TestOptions.detailedHelpMessage) final case class TestOptions( @Recurse shared: SharedOptions = SharedOptions(), @Recurse sharedJava: SharedJavaOptions = SharedJavaOptions(), @Recurse watch: SharedWatchOptions = SharedWatchOptions(), @Recurse compileCross: CrossOptions = CrossOptions(), @Group(HelpGroup.Test.toString) @HelpMessage( """Names of the test frameworks' runner classes to use while running tests. |Skips framework lookup and only runs passed frameworks.""".stripMargin ) @ValueDescription("class-name") @Tag(tags.should) @Tag(tags.inShortHelp) @Name("testFramework") testFrameworks: List[String] = Nil, @Group(HelpGroup.Test.toString) @Tag(tags.should) @Tag(tags.inShortHelp) @HelpMessage("Fail if no test suites were run") requireTests: Boolean = false, @Group(HelpGroup.Test.toString) @Tag(tags.should) @Tag(tags.inShortHelp) @HelpMessage("Specify a glob pattern to filter the tests suite to be run.") testOnly: Option[String] = None ) extends HasSharedOptions object TestOptions { implicit lazy val parser: Parser[TestOptions] = Parser.derive implicit lazy val help: Help[TestOptions] = Help.derive val cmdName = "test" private val helpHeader = "Compile and test Scala code." val helpMessage: String = HelpMessages.shortHelpMessage(cmdName, helpHeader) val detailedHelpMessage: String = s"""$helpHeader | |Test sources are compiled separately (after the 'main' sources), and may use different dependencies, compiler options, and other configurations. |A source file is treated as a test source if: | - the file name ends with `.test.scala` | - the file comes from a directory that is provided as input, and the relative path from that file to its original directory contains a `test` directory | - it contains the `//> using target.scope test` directive (Experimental) | |${HelpMessages.commandConfigurations(cmdName)} | |${HelpMessages.acceptedInputs} | |${HelpMessages.commandDocWebsiteReference(cmdName)}""".stripMargin } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/uninstall/Uninstall.scala ================================================ package scala.cli.commands.uninstall import caseapp.* import scala.build.Logger import scala.cli.commands.ScalaCommand import scala.cli.commands.bloop.BloopExit import scala.cli.commands.uninstallcompletions.{UninstallCompletions, UninstallCompletionsOptions} import scala.cli.commands.update.Update object Uninstall extends ScalaCommand[UninstallOptions] { override def scalaSpecificationLevel = SpecificationLevel.IMPLEMENTATION override def runCommand(options: UninstallOptions, args: RemainingArgs, logger: Logger): Unit = { val interactive = options.bloopExit.global.logging.verbosityOptions.interactiveInstance(forceEnable = true) val binDirPath = options.binDirPath.getOrElse( scala.build.Directories.default().binRepoDir / baseRunnerName ) val destBinPath = binDirPath / options.binaryName val cacheDir = scala.build.Directories.default().cacheDir if ( !Update.isScalaCLIInstalledByInstallationScript && (options.binDir.isEmpty || !options.force) ) { logger.error( s"$fullRunnerName was not installed by the installation script, please use your package manager to uninstall $baseRunnerName." ) sys.exit(1) } if (!options.force) { val fallbackAction = () => { logger.error(s"To uninstall $baseRunnerName pass -f or --force") sys.exit(1) } val msg = s"Do you want to uninstall $baseRunnerName?" interactive.confirmOperation(msg).getOrElse(fallbackAction()) } if (os.exists(destBinPath)) { // uninstall completions logger.debug("Uninstalling completions...") UninstallCompletions.run( UninstallCompletionsOptions(options.sharedUninstallCompletions, options.bloopExit.global), args ) // exit bloop server logger.debug("Stopping Bloop server...") BloopExit.runCommand(options.bloopExit, args, options.global.logging.logger) // remove scala-cli launcher logger.debug(s"Removing $baseRunnerName binary...") os.remove.all(binDirPath) // remove scala-cli caches logger.debug(s"Removing $baseRunnerName cache directory...") if (!options.skipCache) os.remove.all(cacheDir) logger.message("Uninstalled successfully.") } else { logger.error(s"Could't find $baseRunnerName binary at $destBinPath.") sys.exit(1) } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/uninstall/UninstallOptions.scala ================================================ package scala.cli.commands.uninstall import caseapp.* import scala.cli.ScalaCli.{baseRunnerName, fullRunnerName} import scala.cli.commands.bloop.BloopExitOptions import scala.cli.commands.shared.{GlobalOptions, HasGlobalOptions, HelpGroup, HelpMessages} import scala.cli.commands.tags import scala.cli.commands.uninstallcompletions.SharedUninstallCompletionsOptions // format: off @HelpMessage( s"""Uninstalls $fullRunnerName. |Works only when installed with the installation script. |${HelpMessages.installationDocsWebsiteReference}""".stripMargin) final case class UninstallOptions( @Recurse bloopExit: BloopExitOptions = BloopExitOptions(), @Recurse sharedUninstallCompletions: SharedUninstallCompletionsOptions = SharedUninstallCompletionsOptions(), @Group(HelpGroup.Uninstall.toString) @Name("f") @HelpMessage(s"Force $baseRunnerName uninstall") @Tag(tags.implementation) force: Boolean = false, @Hidden @Group(HelpGroup.Uninstall.toString) @HelpMessage(s"Don't clear $fullRunnerName cache") @Tag(tags.implementation) skipCache: Boolean = false, @Hidden @Group(HelpGroup.Uninstall.toString) @HelpMessage("Binary name") @Tag(tags.implementation) binaryName: String = baseRunnerName, @Hidden @Group(HelpGroup.Uninstall.toString) @HelpMessage("Binary directory") @Tag(tags.implementation) binDir: Option[String] = None ) extends HasGlobalOptions { override def global: GlobalOptions = bloopExit.global // format: on lazy val binDirPath: Option[os.Path] = binDir.map(os.Path(_, os.pwd)) } object UninstallOptions { implicit lazy val parser: Parser[UninstallOptions] = Parser.derive implicit lazy val help: Help[UninstallOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/uninstallcompletions/SharedUninstallCompletionsOptions.scala ================================================ package scala.cli.commands.uninstallcompletions import caseapp.* import scala.cli.commands.shared.HelpGroup import scala.cli.commands.tags // format: off final case class SharedUninstallCompletionsOptions( @Group(HelpGroup.Uninstall.toString) @HelpMessage("Path to `*rc` file, defaults to `.bashrc` or `.zshrc` depending on shell") @Tag(tags.implementation) @Tag(tags.inShortHelp) rcFile: Option[String] = None, @Group(HelpGroup.Uninstall.toString) @Hidden @HelpMessage("Custom banner in comment placed in rc file") @Tag(tags.implementation) banner: String = "{NAME} completions", @Group(HelpGroup.Uninstall.toString) @Hidden @HelpMessage("Custom completions name") @Tag(tags.implementation) name: Option[String] = None ) // format: on object SharedUninstallCompletionsOptions { implicit lazy val parser: Parser[SharedUninstallCompletionsOptions] = Parser.derive implicit lazy val help: Help[SharedUninstallCompletionsOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/uninstallcompletions/UninstallCompletions.scala ================================================ package scala.cli.commands.uninstallcompletions import caseapp.* import caseapp.core.help.HelpFormat import java.nio.charset.Charset import scala.build.Logger import scala.build.internals.EnvVar import scala.cli.commands.installcompletions.InstallCompletions import scala.cli.commands.shared.HelpGroup import scala.cli.commands.{ScalaCommand, SpecificationLevel} import scala.cli.internal.ProfileFileUpdater import scala.cli.util.ArgHelpers.* object UninstallCompletions extends ScalaCommand[UninstallCompletionsOptions] { override def scalaSpecificationLevel: SpecificationLevel = SpecificationLevel.IMPLEMENTATION override def helpFormat: HelpFormat = super.helpFormat.withPrimaryGroup(HelpGroup.Uninstall) override def names = List( List("uninstall", "completions"), List("uninstall-completions") ) override def runCommand( options: UninstallCompletionsOptions, args: RemainingArgs, logger: Logger ): Unit = { val name = InstallCompletions.getName(options.shared.name) val zDotDir = EnvVar.Misc.zDotDir.valueOpt .map(os.Path(_, os.pwd)) .getOrElse(os.home) val rcFiles = options.shared.rcFile.map(file => Seq(os.Path(file, os.pwd))).getOrElse(Seq( zDotDir / ".zshrc", os.home / ".bashrc", os.home / ".config" / "fish" / "config.fish" )).filter(os.exists(_)) rcFiles.foreach { rcFile => val banner = options.shared.banner.replace("{NAME}", name) val updated = ProfileFileUpdater.removeFromProfileFile( rcFile.toNIO, banner, Charset.defaultCharset() ) if (options.global.logging.verbosity >= 0) if (updated) { logger.message(s"Updated $rcFile") logger.message(s"$baseRunnerName completions uninstalled successfully") } else logger.error( s"Problem occurred while uninstalling $baseRunnerName completions" ) } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/uninstallcompletions/UninstallCompletionsOptions.scala ================================================ package scala.cli.commands.uninstallcompletions import caseapp.* import scala.cli.ScalaCli.fullRunnerName import scala.cli.commands.shared.{GlobalOptions, HasGlobalOptions, HelpMessages} // format: off @HelpMessage(UninstallCompletionsOptions.helpMessage, "", UninstallCompletionsOptions.detailedHelpMessage) final case class UninstallCompletionsOptions( @Recurse shared: SharedUninstallCompletionsOptions = SharedUninstallCompletionsOptions(), @Recurse global: GlobalOptions = GlobalOptions(), ) extends HasGlobalOptions // format: on object UninstallCompletionsOptions { implicit lazy val parser: Parser[UninstallCompletionsOptions] = Parser.derive implicit lazy val help: Help[UninstallCompletionsOptions] = Help.derive private val helpHeader = s"Uninstalls $fullRunnerName completions from your shell." val helpMessage: String = s"""$helpHeader | |${HelpMessages.commandFullHelpReference("uninstall completions")} |${HelpMessages.commandDocWebsiteReference("completions")}""".stripMargin val detailedHelpMessage: String = s"""$helpHeader | |${HelpMessages.commandDocWebsiteReference("completions")}""".stripMargin } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/update/Update.scala ================================================ package scala.cli.commands.update import caseapp.* import caseapp.core.help.HelpFormat import com.github.plokhotnyuk.jsoniter_scala.core.* import com.github.plokhotnyuk.jsoniter_scala.macros.* import coursier.version.Version import java.net.{HttpURLConnection, URL, URLConnection} import java.nio.charset.StandardCharsets import scala.build.Logger import scala.build.errors.CheckScalaCliVersionError import scala.build.internal.Constants.{ghName, ghOrg, version as scalaCliVersion} import scala.cli.commands.shared.HelpGroup import scala.cli.commands.{CommandUtils, ScalaCommand, SpecificationLevel} import scala.cli.signing.shared.Secret import scala.cli.util.ArgHelpers.* import scala.util.control.NonFatal import scala.util.{Properties, Try} object Update extends ScalaCommand[UpdateOptions] { override def scalaSpecificationLevel: SpecificationLevel = SpecificationLevel.IMPLEMENTATION override def helpFormat: HelpFormat = super.helpFormat.withPrimaryGroup(HelpGroup.Update) private final case class Release( draft: Boolean, prerelease: Boolean, tag_name: String ) { lazy val version: Version = Version(tag_name.stripPrefix("v")) def actualRelease: Boolean = !draft && !prerelease } private lazy val releaseListCodec: JsonValueCodec[List[Release]] = JsonCodecMaker.make def newestScalaCliVersion(tokenOpt: Option[Secret[String]]) : Either[CheckScalaCliVersionError, String] = { // FIXME Do we need paging here? val url = s"https://api.github.com/repos/$ghOrg/$ghName/releases" val headers = Seq("Accept" -> "application/vnd.github.v3+json") ++ tokenOpt.toSeq.map(tk => "Authorization" -> s"token ${tk.value}") try { val resp = download(url, headers*) readFromArray(resp)(using releaseListCodec).filter(_.actualRelease) .maxByOption(_.version) .map(_.version.repr) .toRight(CheckScalaCliVersionError(s"No $fullRunnerName versions found in $url")) } catch { case e: JsonReaderException => Left(CheckScalaCliVersionError(s"Error reading $url", e)) case e: Throwable => Left(CheckScalaCliVersionError( s"Failed to check for the newest Scala CLI version upstream: ${e.getMessage}", e )) } } def installDirPath(options: UpdateOptions): os.Path = options.binDir.map(os.Path(_, os.pwd)).getOrElse( scala.build.Directories.default().binRepoDir / options.binaryName ) private def updateScalaCli(options: UpdateOptions, newVersion: String, logger: Logger): Unit = { val interactive = options.global.logging.verbosityOptions.interactiveInstance(forceEnable = true) if (!options.force) { val fallbackAction = () => { logger.error(s"To update $baseRunnerName to $newVersion pass -f or --force") sys.exit(1) } val msg = s"Do you want to update $baseRunnerName to version $newVersion?" interactive.confirmOperation(msg).getOrElse(fallbackAction()) } val installationScript = downloadFile("https://scala-cli.virtuslab.org/get") // format: off val res = os.proc( "bash", "-s", "--", "--version", newVersion, "--force", "--binary-name", options.binaryName, "--bin-dir", installDirPath(options), ).call( cwd = os.pwd, stdin = installationScript, stdout = os.Inherit, check = false, mergeErrIntoOut = true ) // format: on val output = res.out.trim() if (res.exitCode != 0) { logger.error(s"Error during updating $baseRunnerName: $output") sys.exit(1) } } private def getCurrentVersion(scalaCliBinPath: os.Path): String = { val res = os.proc(scalaCliBinPath, "version", "--cli-version").call(cwd = os.pwd, check = false) if (res.exitCode == 0) res.out.trim() else "0.0.0" } private def update(options: UpdateOptions, currentVersion: String, logger: Logger): Unit = { val newestScalaCliVersion0 = newestScalaCliVersion(options.ghToken.map(_.get())) match { case Left(e) => logger.log(e.message) sys.error(e.message) case Right(v) => v } val isOutdated = CommandUtils.isOutOfDateVersion(newestScalaCliVersion0, currentVersion) if (!options.isInternalRun) if (isOutdated) updateScalaCli(options, newestScalaCliVersion0, logger) else println(s"$fullRunnerName is up-to-date") else if (isOutdated) println( s"""Your $fullRunnerName $currentVersion is outdated, please update $fullRunnerName to $newestScalaCliVersion0 |Run 'curl -sSLf https://scala-cli.virtuslab.org/get | sh' to update $fullRunnerName.""".stripMargin ) } override def runCommand( options: UpdateOptions, remainingArgs: RemainingArgs, logger: Logger ): Unit = checkUpdate(options, logger) def checkUpdate(options: UpdateOptions, logger: Logger): Unit = { val scalaCliBinPath = installDirPath(options) / options.binaryName val programName = argvOpt.flatMap(_.headOption).getOrElse { sys.error("update called in a non-standard way :|") } lazy val isScalaCliInPath = // if binDir is non empty, we not except scala-cli in PATH, it is useful in tests CommandUtils.getAbsolutePathToScalaCli(programName).contains( installDirPath(options).toString() ) || options.binDir.isDefined if (!os.exists(scalaCliBinPath) || !isScalaCliInPath) { if (!options.isInternalRun) { logger.error( s"$fullRunnerName was not installed by the installation script, please use your package manager to update $baseRunnerName." ) sys.exit(1) } } else if (Properties.isWin) { if (!options.isInternalRun) { logger.error(s"$fullRunnerName update is not supported on Windows.") sys.exit(1) } } else if (options.binaryName == baseRunnerName) update(options, scalaCliVersion, logger) else update(options, getCurrentVersion(scalaCliBinPath), logger) } def checkUpdateSafe(logger: Logger): Unit = try // log about update only if scala-cli was installed from installation script if (isScalaCLIInstalledByInstallationScript) checkUpdate(UpdateOptions(isInternalRun = true), logger) catch { case NonFatal(ex) => logger.debug(s"Ignoring error during checking update: $ex") } def isScalaCLIInstalledByInstallationScript: Boolean = { val classesDir = getClass.getProtectionDomain.getCodeSource.getLocation.toURI.toString val binRepoDir = build.Directories.default().binRepoDir.toString() classesDir.contains(binRepoDir) } // from https://github.com/coursier/coursier/blob/7b7c2c312aea26e850f0cd2cf15e688d0777f819/modules/cache/jvm/src/main/scala/coursier/cache/CacheUrl.scala#L489-L497 private def closeConn(conn: URLConnection): Unit = { Try(conn.getInputStream).toOption.filter(_ != null).foreach(_.close()) conn match { case conn0: HttpURLConnection => Try(conn0.getErrorStream).toOption.filter(_ != null).foreach(_.close()) conn0.disconnect() case _ => } } private def download( url: String, headers: (String, String)* ): Array[Byte] = { var conn: URLConnection = null val url0 = new URL(url) try { conn = url0.openConnection() for ((k, v) <- headers) conn.setRequestProperty(k, v) conn.getInputStream.readAllBytes() } catch { case NonFatal(ex) => throw new RuntimeException(s"Error downloading $url: $ex", ex) } finally if (conn != null) closeConn(conn) } private def downloadFile(url: String): String = { val data = download(url) new String(data, StandardCharsets.UTF_8) } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/update/UpdateOptions.scala ================================================ package scala.cli.commands.update import caseapp.* import scala.cli.ScalaCli.{baseRunnerName, fullRunnerName} import scala.cli.commands.shared.{GlobalOptions, HasGlobalOptions, HelpGroup, HelpMessages} import scala.cli.commands.tags import scala.cli.signing.shared.PasswordOption import scala.cli.signing.util.ArgParsers.* // format: off @HelpMessage(UpdateOptions.helpMessage, "", UpdateOptions.detailedHelpMessage) final case class UpdateOptions( @Recurse global: GlobalOptions = GlobalOptions(), @Hidden @Group(HelpGroup.Update.toString) @HelpMessage("Binary name") @Tag(tags.implementation) binaryName: String = baseRunnerName, @Hidden @Group(HelpGroup.Update.toString) @HelpMessage("Binary directory") @Tag(tags.implementation) binDir: Option[String] = None, @Name("f") @Group(HelpGroup.Update.toString) @HelpMessage(s"Force update $fullRunnerName if it is outdated") @Tag(tags.implementation) @Tag(tags.inShortHelp) force: Boolean = false, @Hidden @Tag(tags.implementation) isInternalRun: Boolean = false, @Hidden @HelpMessage(HelpMessages.passwordOption) @Tag(tags.implementation) ghToken: Option[PasswordOption] = None ) extends HasGlobalOptions // format: on object UpdateOptions { implicit lazy val parser: Parser[UpdateOptions] = Parser.derive implicit lazy val help: Help[UpdateOptions] = Help.derive private val helpHeader = s"""Updates $fullRunnerName. |Works only when installed with the installation script. |If $fullRunnerName was installed with an external tool, refer to its update methods.""".stripMargin val helpMessage: String = s"""$helpHeader | |${HelpMessages.commandFullHelpReference("update")} |${HelpMessages.installationDocsWebsiteReference}""".stripMargin val detailedHelpMessage: String = s"""$helpHeader | |${HelpMessages.installationDocsWebsiteReference}""".stripMargin } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/util/BuildCommandHelpers.scala ================================================ package scala.cli.commands.util import scala.build.errors.BuildException import scala.build.{Build, Builds, CrossBuildParams, Logger, Os} import scala.cli.commands.ScalaCommand import scala.cli.commands.shared.SharedOptions import scala.cli.commands.util.ScalacOptionsUtil.* trait BuildCommandHelpers { self: ScalaCommand[?] => extension (b: Seq[Build.Successful]) { def groupedByCrossParams: Map[CrossBuildParams, Seq[Build.Successful]] = b.groupBy(bb => CrossBuildParams(bb.scalaParams, bb.options)) } extension (successfulBuild: Build.Successful) { def retainedMainClass( logger: Logger, mainClasses: Seq[String] = successfulBuild.foundMainClasses() ): Either[BuildException, String] = successfulBuild.retainedMainClass( mainClasses, self.argvOpt.map(_.mkString(" ")).getOrElse(actualFullCommand), logger ) } extension (builds: Builds) { def anyBuildCancelled: Boolean = builds.all.exists { case _: Build.Cancelled => true case _ => false } def anyBuildFailed: Boolean = builds.all.exists { case _: Build.Failed => true case _ => false } } } object BuildCommandHelpers { extension (successfulBuild: Build.Successful) { /** -O -d defaults to --compile-output; if both are defined, --compile-output takes precedence */ def copyOutput(sharedOptions: SharedOptions): Unit = sharedOptions.compilationOutput.filter(_.nonEmpty) .orElse(sharedOptions.scalacOptions.getScalacOption("-d")) .filter(_.nonEmpty) .map(os.Path(_, Os.pwd)).foreach { output => os.copy( successfulBuild.output, output, createFolders = true, mergeFolders = true, replaceExisting = true ) } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/util/CommandHelpers.scala ================================================ package scala.cli.commands.util import scala.build.Logger import scala.build.errors.BuildException trait CommandHelpers { implicit class EitherBuildExceptionOps[E <: BuildException, T](private val either: Either[E, T]) { def orReport(logger: Logger): Option[T] = either match { case Left(ex) => logger.log(ex) None case Right(t) => Some(t) } def orExit(logger: Logger): T = either match { case Left(ex) => logger.exit(ex) case Right(t) => t } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/util/HelpUtils.scala ================================================ package scala.cli.commands.util import caseapp.core.help.{Help, HelpFormat} object HelpUtils { extension (help: Help[?]) { private def abstractHelp( format: HelpFormat, showHidden: Boolean )(f: (StringBuilder, HelpFormat, Boolean) => Unit): String = { val b = new StringBuilder f(b, format, showHidden) b.result() } def optionsHelp(format: HelpFormat, showHidden: Boolean = false): String = abstractHelp(format, showHidden)(help.printOptions) } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/util/JvmUtils.scala ================================================ package scala.cli.commands package util import java.io.File import scala.build.EitherCps.{either, value} import scala.build.errors.{BuildException, JvmDownloadError, UnrecognizedDebugModeError} import scala.build.internal.CsLoggerUtil.* import scala.build.internal.OsLibc import scala.build.options.BuildOptions.JavaHomeInfo import scala.build.options.{JavaOpt, JavaOptions, ShadowingSeq} import scala.build.{Os, Position, Positioned, options as bo} import scala.cli.commands.shared.{CoursierOptions, SharedJvmOptions, SharedOptions} import scala.concurrent.ExecutionContextExecutorService import scala.util.{Failure, Properties, Success, Try} object JvmUtils { def javaOptions(opts: SharedJvmOptions): Either[BuildException, JavaOptions] = either { import opts._ val (javacFilePlugins, javacPluginDeps) = javacPlugin .filter(_.trim.nonEmpty) .partition { input => input.contains(File.separator) || (Properties.isWin && input.contains("/")) || input.count(_ == ':') < 2 } val javaOptsSeq = { val isDebug = opts.sharedDebug.debug || opts.sharedDebug.debugMode.nonEmpty || opts.sharedDebug.debugPort.nonEmpty if (isDebug) { val server = value { opts.sharedDebug.debugMode match { case Some("attach") | Some("a") | None => Right("y") case Some("listen") | Some("l") => Right("n") case Some(m) => Left(new UnrecognizedDebugModeError(m)) } } val port = opts.sharedDebug.debugPort.getOrElse("5005") Seq(Positioned.none( JavaOpt(s"-agentlib:jdwp=transport=dt_socket,server=$server,suspend=y,address=$port") )) } else Seq.empty } JavaOptions( javaHomeOpt = javaHome.filter(_.nonEmpty).map(v => Positioned(Seq(Position.CommandLine("--java-home")), os.Path(v, Os.pwd)) ), jvmIdOpt = jvm.filter(_.nonEmpty).map(Positioned.commandLine), jvmIndexOpt = jvmIndex.filter(_.nonEmpty), jvmIndexOs = jvmIndexOs.map(_.trim).filter(_.nonEmpty), jvmIndexArch = jvmIndexArch.map(_.trim).filter(_.nonEmpty), javaOpts = ShadowingSeq.from(javaOptsSeq), javacPluginDependencies = SharedOptions.parseDependencies( javacPluginDeps.map(Positioned.none(_)), ignoreErrors = false ), javacPlugins = javacFilePlugins.map(s => Positioned.none(os.Path(s, Os.pwd))), javacOptions = javacOption.map(Positioned.commandLine) ) } def downloadJvm(jvmId: String, options: bo.BuildOptions): Either[BuildException, JavaHomeInfo] = { implicit val ec: ExecutionContextExecutorService = options.finalCache.ec val javaHomeManager = options.javaHomeManager .withMessage(s"Downloading JVM $jvmId") javaHomeManager.cache .flatMap(_.archiveCache.cache.loggerOpt) .getOrElse(_root_.coursier.cache.CacheLogger.nop) val javaHomeOrError = Try(javaHomeManager.get(jvmId).unsafeRun()) match { case Success(path) => Right(path) case Failure(e) => Left(JvmDownloadError(jvmId, e)) } for { javaHome <- javaHomeOrError } yield { val javaHomePath = os.Path(javaHome) val (javaVersion, javaCmd) = OsLibc.javaHomeVersion(javaHomePath) JavaHomeInfo(javaHomePath, javaCmd, javaVersion) } } def getJavaCmdVersionOrHigher( javaVersion: Int, options: bo.BuildOptions ): Either[BuildException, JavaHomeInfo] = { val javaHomeCmdOpt = for { javaHome <- options.javaHomeLocationOpt() (javaHomeVersion, javaHomeCmd) = OsLibc.javaHomeVersion(javaHome.value) if javaHomeVersion >= javaVersion } yield JavaHomeInfo(javaHome.value, javaHomeCmd, javaHomeVersion) javaHomeCmdOpt match { case Some(cmd) => Right(cmd) case None => downloadJvm(javaVersion.toString, options) } } def getJavaCmdVersionOrHigher( javaVersion: Int, jvmOpts: SharedJvmOptions, coursierOpts: CoursierOptions ): Either[BuildException, JavaHomeInfo] = { val sharedOpts = SharedOptions(jvm = jvmOpts, coursier = coursierOpts) for { options <- sharedOpts.buildOptions() javaCmd <- getJavaCmdVersionOrHigher(javaVersion, options) } yield javaCmd } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/util/RunHadoop.scala ================================================ package scala.cli.commands.util import scala.build.EitherCps.{either, value} import scala.build.errors.BuildException import scala.build.internal.Runner import scala.build.{Build, Logger} import scala.cli.commands.package0.Package as PackageCmd import scala.cli.commands.packaging.Spark object RunHadoop { def run( builds: Seq[Build.Successful], mainClass: String, args: Seq[String], logger: Logger, allowExecve: Boolean, showCommand: Boolean, scratchDirOpt: Option[os.Path] ): Either[BuildException, Either[Seq[String], (Process, Option[() => Unit])]] = either { // FIXME Get Spark.hadoopModules via provided settings? val providedModules = Spark.hadoopModules scratchDirOpt.foreach(os.makeDir.all(_)) val assembly = os.temp( dir = scratchDirOpt.orNull, prefix = "hadoop-job", suffix = ".jar", deleteOnExit = scratchDirOpt.isEmpty ) value { PackageCmd.assembly( builds, assembly, // "hadoop jar" doesn't accept a main class as second argument if the jar as first argument has a main class in its manifest… None, providedModules, withPreamble = false, () => Right(()), logger ) } val javaOpts = builds.head.options.javaOptions.javaOpts.toSeq.map(_.value.value) val extraEnv = if javaOpts.isEmpty then Map[String, String]() else Map( "HADOOP_CLIENT_OPTS" -> javaOpts.mkString(" ") // no escaping… ) val hadoopJarCommand = Seq("hadoop", "jar") val finalCommand = hadoopJarCommand ++ Seq(assembly.toString, mainClass) ++ args if showCommand then Left(Runner.envCommand(extraEnv) ++ finalCommand) else { val proc = if allowExecve then Runner.maybeExec("hadoop", finalCommand, logger, extraEnv = extraEnv) else Runner.run(finalCommand, logger, extraEnv = extraEnv) Right(( proc, if scratchDirOpt.isEmpty then Some(() => os.remove(assembly, checkExists = true)) else None )) } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/util/RunSpark.scala ================================================ package scala.cli.commands.util import scala.build.EitherCps.{either, value} import scala.build.errors.BuildException import scala.build.internal.Runner import scala.build.internals.EnvVar import scala.build.{Build, Logger} import scala.cli.commands.package0.Package as PackageCmd import scala.cli.commands.packaging.Spark import scala.cli.packaging.Library import scala.util.Properties object RunSpark { def run( builds: Seq[Build.Successful], mainClass: String, args: Seq[String], submitArgs: Seq[String], logger: Logger, allowExecve: Boolean, showCommand: Boolean, scratchDirOpt: Option[os.Path] ): Either[BuildException, Either[Seq[String], (Process, Option[() => Unit])]] = either { // FIXME Get Spark.sparkModules via provided settings? val providedModules = Spark.sparkModules val providedFiles = value(PackageCmd.providedFiles(builds, providedModules, logger)).toSet val depCp = builds.flatMap(_.dependencyClassPath).distinct.filterNot(providedFiles) val javaHomeInfo = builds.head.options.javaHome().value val javaOpts = builds.head.options.javaOptions.javaOpts.toSeq.map(_.value.value) val ext = if Properties.isWin then ".cmd" else "" val submitCommand: String = EnvVar.Spark.sparkHome.valueOpt .map(os.Path(_, os.pwd)) .map(_ / "bin" / s"spark-submit$ext") .filter(os.exists(_)) .map(_.toString) .getOrElse(s"spark-submit$ext") val jarsArgs = if (depCp.isEmpty) Nil else Seq("--jars", depCp.mkString(",")) scratchDirOpt.foreach(os.makeDir.all(_)) val library = Library.libraryJar(builds) val finalCommand = Seq(submitCommand, "--class", mainClass) ++ jarsArgs ++ javaOpts.flatMap(opt => Seq("--driver-java-options", opt)) ++ submitArgs ++ Seq(library.toString) ++ args val envUpdates = javaHomeInfo.envUpdates(sys.env) if showCommand then Left(Runner.envCommand(envUpdates) ++ finalCommand) else { val proc = if allowExecve then Runner.maybeExec("spark-submit", finalCommand, logger, extraEnv = envUpdates) else Runner.run(finalCommand, logger, extraEnv = envUpdates) Right(( proc, if scratchDirOpt.isEmpty then Some(() => os.remove(library, checkExists = true)) else None )) } } def runStandalone( builds: Seq[Build.Successful], mainClass: String, args: Seq[String], submitArgs: Seq[String], logger: Logger, allowExecve: Boolean, showCommand: Boolean, scratchDirOpt: Option[os.Path] ): Either[BuildException, Either[Seq[String], (Process, Option[() => Unit])]] = either { // FIXME Get Spark.sparkModules via provided settings? val providedModules = Spark.sparkModules val sparkClassPath: Seq[os.Path] = value(PackageCmd.providedFiles( builds, providedModules, logger )) scratchDirOpt.foreach(os.makeDir.all(_)) val library = Library.libraryJar(builds) val finalMainClass = "org.apache.spark.deploy.SparkSubmit" val depCp = builds.flatMap(_.dependencyClassPath).distinct.filterNot(sparkClassPath.toSet) val javaHomeInfo = builds.head.options.javaHome().value val javaOpts = builds.head.options.javaOptions.javaOpts.toSeq.map(_.value.value) val jarsArgs = if depCp.isEmpty then Nil else Seq("--jars", depCp.mkString(",")) val finalArgs = Seq("--class", mainClass) ++ jarsArgs ++ javaOpts.flatMap(opt => Seq("--driver-java-options", opt)) ++ submitArgs ++ Seq(library.toString) ++ args val envUpdates = javaHomeInfo.envUpdates(sys.env) if showCommand then Left { Runner.jvmCommand( javaHomeInfo.javaCommand, javaOpts, sparkClassPath, finalMainClass, finalArgs, extraEnv = envUpdates, useManifest = builds.head.options.notForBloopOptions.runWithManifest, scratchDirOpt = scratchDirOpt ) } else { val proc = Runner.runJvm( javaHomeInfo.javaCommand, javaOpts, sparkClassPath, finalMainClass, finalArgs, logger, allowExecve = allowExecve, extraEnv = envUpdates, useManifest = builds.head.options.notForBloopOptions.runWithManifest, scratchDirOpt = scratchDirOpt ) Right(( proc, if scratchDirOpt.isEmpty then Some(() => os.remove(library, checkExists = true)) else None )) } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/util/ScalaCliSttpBackend.scala ================================================ package scala.cli.commands.util import sttp.capabilities.Effect import sttp.client3.* import sttp.monad.MonadError import scala.build.Logger import scala.concurrent.duration import scala.concurrent.duration.FiniteDuration import scala.util.Try class ScalaCliSttpBackend( underlying: SttpBackend[Identity, Any], logger: Logger ) extends SttpBackend[Identity, Any] { override def send[T, R >: Any & Effect[Identity]](request: Request[T, R]): Response[T] = { logger.debug(s"HTTP ${request.method} ${request.uri}") if (logger.verbosity >= 3) logger.debug(s"request: '${request.show()}'") val resp = underlying.send[T, R](request) logger.debug(s"HTTP ${request.method} ${request.uri}: HTTP ${resp.code} ${resp.statusText}") if (logger.verbosity >= 3) { val logResp = request.response match { case ResponseAsByteArray => resp.copy( body = Try(new String(resp.body.asInstanceOf[Array[Byte]])) ) case _ => resp } logger.debug(s"response: '${logResp.show()}'") } resp } override def close(): Unit = underlying.close() override def responseMonad: MonadError[Identity] = underlying.responseMonad } object ScalaCliSttpBackend { def httpURLConnection(logger: Logger, timeoutSeconds: Option[Int] = None): ScalaCliSttpBackend = new ScalaCliSttpBackend( HttpURLConnectionBackend( options = timeoutSeconds .fold(SttpBackendOptions.Default)(t => SttpBackendOptions.connectionTimeout(FiniteDuration(t, duration.SECONDS)) ) ), logger ) } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/util/ScalacOptionsUtil.scala ================================================ package scala.cli.commands.util import scala.build.options.ScalacOpt.{filterScalacOptionKeys, noDashPrefixes} import scala.build.options.{ScalacOpt, ShadowingSeq} import scala.cli.commands.shared.{ScalacExtraOptions, ScalacOptions} object ScalacOptionsUtil { extension (opts: List[String]) { def withScalacExtraOptions(scalacExtra: ScalacExtraOptions): List[String] = { def maybeScalacExtraOption( get: ScalacExtraOptions => Boolean, scalacName: String ): Option[String] = if get(scalacExtra) && !opts.contains(scalacName) then Some(scalacName) else None val scalacHelp = maybeScalacExtraOption(_.scalacHelp, "-help") val scalacVerbose = maybeScalacExtraOption(_.scalacVerbose, "-verbose") opts ++ scalacHelp ++ scalacVerbose } def toScalacOptShadowingSeq: ShadowingSeq[ScalacOpt] = ShadowingSeq.from(opts.filter(_.nonEmpty).map(ScalacOpt(_))) def getScalacPrefixOption(prefixKey: String): Option[String] = opts.find(_.startsWith(s"$prefixKey:")).map(_.stripPrefix(s"$prefixKey:")) def getScalacOption(key: String): Option[String] = opts.toScalacOptShadowingSeq.getOption(key) } extension (opts: ShadowingSeq[ScalacOpt]) { def filterNonRedirected: ShadowingSeq[ScalacOpt] = opts.filterScalacOptionKeys(k => !ScalacOptions.ScalaCliRedirectedOptions.contains(k.noDashPrefixes) ) def filterNonDeprecated: ShadowingSeq[ScalacOpt] = opts.filterScalacOptionKeys(k => !ScalacOptions.ScalacDeprecatedOptions.contains(k.noDashPrefixes) ) def getOption(key: String): Option[String] = opts.get(ScalacOpt(key)).headOption.map(_.value) } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/version/Version.scala ================================================ package scala.cli.commands.version import caseapp.* import caseapp.core.help.HelpFormat import scala.build.Logger import scala.build.internal.Constants import scala.cli.ScalaCli import scala.cli.commands.shared.{HelpCommandGroup, HelpGroup} import scala.cli.commands.update.Update import scala.cli.commands.{CommandUtils, ScalaCommand, SpecificationLevel} import scala.cli.util.ArgHelpers.* object Version extends ScalaCommand[VersionOptions] { override def group: String = HelpCommandGroup.Miscellaneous.toString override def scalaSpecificationLevel: SpecificationLevel = SpecificationLevel.SHOULD override def helpFormat: HelpFormat = super.helpFormat .withHiddenGroup(HelpGroup.Logging) .withHiddenGroupWhenShowHidden(HelpGroup.Logging) .withPrimaryGroup(HelpGroup.Version) override def runCommand(options: VersionOptions, args: RemainingArgs, logger: Logger): Unit = { lazy val maybeNewerScalaCliVersion: Option[String] = Update.newestScalaCliVersion(options.ghToken.map(_.get())) match { case Left(e) => logger.debug(e.message) None case Right(newestScalaCliVersion) => if CommandUtils.isOutOfDateVersion(newestScalaCliVersion, Constants.version) then Some(newestScalaCliVersion) else None } if options.cliVersion then println(Constants.version) else if options.scalaVersion then println(defaultScalaVersion) else { println(versionInfo) val skipCliUpdates = ScalaCli.launcherOptions.scalaRunner.skipCliUpdates.getOrElse(false) if !options.offline && !skipCliUpdates then maybeNewerScalaCliVersion.foreach { v => logger.message( s"""Your $fullRunnerName version is outdated. The newest version is $v |It is recommended that you update $fullRunnerName through the same tool or method you used for its initial installation for avoiding the creation of outdated duplicates.""".stripMargin ) } } } private def versionInfo: String = val version = Constants.version val detailedVersionOpt = Constants.detailedVersion.filter(_ != version).fold("")(" (" + _ + ")") s"""$fullRunnerName version: $version$detailedVersionOpt |Scala version (default): $defaultScalaVersion""".stripMargin } ================================================ FILE: modules/cli/src/main/scala/scala/cli/commands/version/VersionOptions.scala ================================================ package scala.cli.commands.version import caseapp.* import scala.cli.ScalaCli.fullRunnerName import scala.cli.commands.shared.{GlobalOptions, HasGlobalOptions, HelpGroup, HelpMessages} import scala.cli.commands.tags import scala.cli.signing.shared.PasswordOption import scala.cli.signing.util.ArgParsers.* // format: off @HelpMessage(VersionOptions.helpMessage, "", VersionOptions.detailedHelpMessage) final case class VersionOptions( @Recurse global: GlobalOptions = GlobalOptions(), @Tag(tags.implementation) @Tag(tags.inShortHelp) @Group(HelpGroup.Version.toString) @HelpMessage(s"Show plain $fullRunnerName version only") @Name("cli") cliVersion: Boolean = false, @Group(HelpGroup.Version.toString) @HelpMessage("Show plain Scala version only") @Tag(tags.implementation) @Tag(tags.inShortHelp) @Name("scala") scalaVersion: Boolean = false, @Hidden @HelpMessage(HelpMessages.passwordOption) @Tag(tags.implementation) ghToken: Option[PasswordOption] = None, @Group(HelpGroup.Version.toString) @Tag(tags.implementation) @Tag(tags.inShortHelp) @HelpMessage(s"Don't check for the newest available $fullRunnerName version upstream") offline: Boolean = false ) extends HasGlobalOptions // format: on object VersionOptions { implicit lazy val parser: Parser[VersionOptions] = Parser.derive implicit lazy val help: Help[VersionOptions] = Help.derive val cmdName = "version" private val helpHeader = s"Prints the version of the $fullRunnerName and the default version of Scala." val helpMessage: String = HelpMessages.shortHelpMessage(cmdName, helpHeader) val detailedHelpMessage: String = s"""$helpHeader (which can be overridden in the project) |If network connection is available, this sub-command also checks if the installed $fullRunnerName is up-to-date. | |The version of the $fullRunnerName is the version of the command-line tool that runs Scala programs, which |is distinct from the Scala version of the compiler. We recommend to specify the version of the Scala compiler |for a project in its sources (via a using directive). Otherwise, $fullRunnerName falls back to the default |Scala version defined by the runner. | |${HelpMessages.commandDocWebsiteReference(cmdName)}""".stripMargin } ================================================ FILE: modules/cli/src/main/scala/scala/cli/errors/FailedToSignFileError.scala ================================================ package scala.cli.errors import scala.build.errors.BuildException final class FailedToSignFileError(val path: Either[String, os.Path], val error: String) extends BuildException(s"Failed to sign ${path.fold(identity, _.toString)}: $error") ================================================ FILE: modules/cli/src/main/scala/scala/cli/errors/FoundVirtualInputsError.scala ================================================ package scala.cli.errors import scala.build.errors.BuildException import scala.build.input.Virtual final class FoundVirtualInputsError( val virtualInputs: Seq[Virtual] ) extends BuildException( s"Found virtual inputs: ${virtualInputs.map(_.source).mkString(", ")}" ) { assert(virtualInputs.nonEmpty) } ================================================ FILE: modules/cli/src/main/scala/scala/cli/errors/GitHubApiError.scala ================================================ package scala.cli.errors import scala.build.errors.BuildException final class GitHubApiError(msg: String) extends BuildException(msg) ================================================ FILE: modules/cli/src/main/scala/scala/cli/errors/GraalVMNativeImageError.scala ================================================ package scala.cli.errors import scala.build.errors.BuildException final class GraalVMNativeImageError() extends BuildException(s"Error building native image with GraalVM") ================================================ FILE: modules/cli/src/main/scala/scala/cli/errors/InvalidSonatypePublishCredentials.scala ================================================ package scala.cli.errors import scala.build.errors.BuildException final class InvalidSonatypePublishCredentials(usernameIsAscii: Boolean, passwordIsAscii: Boolean) extends BuildException( if (usernameIsAscii && passwordIsAscii) "Username or password to the publish repository are incorrect" else s"Your Sonatype ${InvalidSonatypePublishCredentials.isUsernameOrPassword( usernameIsAscii, passwordIsAscii )} unsupported characters" ) object InvalidSonatypePublishCredentials { def isUsernameOrPassword(usernameIsAscii: Boolean, passwordIsAscii: Boolean): String = if (!usernameIsAscii && !passwordIsAscii) "password and username contain" else if (!usernameIsAscii) "username contains" else "password contains" } ================================================ FILE: modules/cli/src/main/scala/scala/cli/errors/MalformedChecksumsError.scala ================================================ package scala.cli.errors import scala.build.errors.BuildException final class MalformedChecksumsError(input: Seq[String], errors: ::[String]) extends BuildException( s"Malformed checksums: ${errors.mkString(", ")} for inputs: ${input.mkString(", ")}" ) ================================================ FILE: modules/cli/src/main/scala/scala/cli/errors/MalformedOptionError.scala ================================================ package scala.cli.errors import scala.build.errors.BuildException final class MalformedOptionError( val optionName: String, val optionValue: String, val expected: String ) extends BuildException( { val q = "\"" s"Malformed option $optionName: got $q$optionValue$q, expected $q$expected$q" } ) ================================================ FILE: modules/cli/src/main/scala/scala/cli/errors/MissingConfigEntryError.scala ================================================ package scala.cli.errors import scala.build.errors.BuildException final class MissingConfigEntryError(key: String) extends BuildException(s"Missing config entry $key") ================================================ FILE: modules/cli/src/main/scala/scala/cli/errors/MissingPublishOptionError.scala ================================================ package scala.cli.errors import scala.build.errors.BuildException final class MissingPublishOptionError( val name: String, val optionName: String, val directiveName: String, val configKeys: Seq[String] = Nil, val extraMessage: String = "" ) extends BuildException( { val directivePart = if (directiveName.isEmpty) "" else s" or with a '//> using $directiveName' directive" val configPart = if (configKeys.isEmpty) "" else s" or by setting ${configKeys.mkString(", ")} in the configuration" val extraPart = if (extraMessage.isEmpty) "" else s", ${extraMessage.dropWhile(_.isWhitespace)}" s"Missing $name for publishing, specify one with $optionName" + directivePart + configPart + extraPart } ) object MissingPublishOptionError { def versionError = new MissingPublishOptionError("version", "--project-version", "publish.version") def repositoryError = new MissingPublishOptionError("repository", "--publish-repository", "publish.repository") } ================================================ FILE: modules/cli/src/main/scala/scala/cli/errors/PgpError.scala ================================================ package scala.cli.errors import scala.build.errors.BuildException final class PgpError(message: String) extends BuildException(message) ================================================ FILE: modules/cli/src/main/scala/scala/cli/errors/ScalaJsLinkingError.scala ================================================ package scala.cli.errors import scala.build.errors.BuildException final class ScalaJsLinkingError( val expected: os.RelPath, val foundFiles: Seq[os.RelPath] ) extends BuildException( s"Error: $expected not found after Scala.js linking " + (if (foundFiles.isEmpty) "(no files found)" else s"(found ${foundFiles.mkString(", ")})") ) ================================================ FILE: modules/cli/src/main/scala/scala/cli/errors/ScaladocGenerationFailedError.scala ================================================ package scala.cli.errors import scala.build.errors.BuildException final class ScaladocGenerationFailedError(val retCode: Int) extends BuildException(s"Scaladoc generation failed (exit code: $retCode)") { assert(retCode != 0) } ================================================ FILE: modules/cli/src/main/scala/scala/cli/errors/UploadError.scala ================================================ package scala.cli.errors import coursier.publish.Content import coursier.publish.fileset.Path import coursier.publish.upload.Upload import scala.build.errors.BuildException final class UploadError(errors: ::[(Path, Content, Upload.Error)]) extends BuildException( s"Error uploading ${errors.length} file(s):" + errors .map { case (path, _, err) => System.lineSeparator() + s" ${path.repr}: ${err.getMessage}" } .mkString ) ================================================ FILE: modules/cli/src/main/scala/scala/cli/errors/WrongSonatypeServerError.scala ================================================ package scala.cli.errors import scala.build.errors.BuildException final class WrongSonatypeServerError(legacyChosen: Boolean) extends BuildException( s"Wrong Sonatype server, try ${if legacyChosen then "'central-s01'" else "'central'"}" ) ================================================ FILE: modules/cli/src/main/scala/scala/cli/exportCmd/JsonProject.scala ================================================ package scala.cli.exportCmd import com.github.plokhotnyuk.jsoniter_scala.core.{JsonValueCodec, WriterConfig, writeToStream} import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker import java.io.PrintStream import scala.build.info.{BuildInfo, ExportDependencyFormat, ScopedBuildInfo} import scala.util.Using final case class JsonProject(buildInfo: BuildInfo) extends Project { def sorted = this.copy( buildInfo = buildInfo.copy( scopes = buildInfo.scopes.map { case (k, v) => k -> v.sorted } ) ) def withEmptyScopesRemoved = this.copy( buildInfo = buildInfo.copy( scopes = buildInfo.scopes.filter(_._2 != ScopedBuildInfo.empty) ) ) def writeTo(dir: os.Path): Unit = { val config = WriterConfig.withIndentionStep(1) Using(os.write.outputStream(dir / "export.json")) { outputStream => writeToStream( sorted.withEmptyScopesRemoved.buildInfo, outputStream, config )( using JsonProject.jsonCodecBuildInfo ) } } def print(printStream: PrintStream): Unit = { val config = WriterConfig.withIndentionStep(1) writeToStream( sorted.withEmptyScopesRemoved.buildInfo, printStream, config )( using JsonProject.jsonCodecBuildInfo ) } } extension (s: ScopedBuildInfo) { def sorted(using ord: Ordering[String]) = s.copy( s.sources.sorted, s.scalacOptions.sorted, s.scalaCompilerPlugins.sorted(using JsonProject.ordering), s.dependencies.sorted(using JsonProject.ordering), s.compileOnlyDependencies.sorted(using JsonProject.ordering), s.resolvers.sorted, s.resourceDirs.sorted, s.customJarsDecls.sorted ) } object JsonProject { implicit lazy val jsonCodecBuildInfo: JsonValueCodec[BuildInfo] = JsonCodecMaker.make implicit lazy val jsonCodecScopedBuildInfo: JsonValueCodec[ScopedBuildInfo] = JsonCodecMaker.make implicit val ordering: Ordering[ExportDependencyFormat] = Ordering.by(x => x.groupId + x.artifactId.fullName) implicit lazy val jsonCodecExportDependencyFormat: JsonValueCodec[ExportDependencyFormat] = JsonCodecMaker.make } ================================================ FILE: modules/cli/src/main/scala/scala/cli/exportCmd/JsonProjectDescriptor.scala ================================================ package scala.cli.exportCmd import scala.build.errors.BuildException import scala.build.info.{BuildInfo, ScopedBuildInfo} import scala.build.options.{BuildOptions, Scope} import scala.build.{Logger, Sources} final case class JsonProjectDescriptor( projectName: Option[String] = None, workspace: os.Path, logger: Logger ) extends ProjectDescriptor { def `export`( optionsMain: BuildOptions, optionsTest: BuildOptions, sourcesMain: Sources, sourcesTest: Sources ): Either[BuildException, JsonProject] = { def getScopedBuildInfo(options: BuildOptions, sources: Sources) = val sourcePaths = sources.paths.map(_._1.toString) val inMemoryPaths = sources.inMemory.flatMap(_.originalPath.toSeq.map(_._2.toString)) ScopedBuildInfo(options, sourcePaths ++ inMemoryPaths) for { baseBuildInfo <- BuildInfo(optionsMain, workspace) mainBuildInfo = getScopedBuildInfo(optionsMain, sourcesMain) testBuildInfo = getScopedBuildInfo(optionsTest, sourcesTest) } yield JsonProject(baseBuildInfo .withScope(Scope.Main.name, mainBuildInfo) .withScope(Scope.Test.name, testBuildInfo)) } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/exportCmd/MavenProject.scala ================================================ package scala.cli.exportCmd import java.nio.charset.StandardCharsets import scala.build.options.ConfigMonoid import scala.xml.{Elem, NodeSeq, PrettyPrinter} final case class MavenProject( groupId: Option[String] = None, artifactId: Option[String] = None, version: Option[String] = None, plugins: Seq[MavenPlugin] = Nil, imports: Seq[String] = Nil, settings: Seq[Seq[String]] = Nil, dependencies: Seq[MavenLibraryDependency] = Nil, mainSources: Seq[(os.SubPath, String, Array[Byte])] = Nil, testSources: Seq[(os.SubPath, String, Array[Byte])] = Nil, resourceDirectories: Seq[String] = Nil ) extends Project { def +(other: MavenProject): MavenProject = MavenProject.monoid.orElse(this, other) def writeTo(dir: os.Path): Unit = { System.lineSeparator() val charset = StandardCharsets.UTF_8 val buildMavenContent = MavenModel( "4.0.0", groupId.getOrElse("groupId"), artifactId.getOrElse("artifactId"), version.getOrElse("0.1-SNAPSHOT"), dependencies, plugins, resourceDirectories ) val prettyPrinter = new PrettyPrinter(width = 80, step = 2) val formattedXml = prettyPrinter.format(buildMavenContent.toXml) os.write( dir / "pom.xml", formattedXml.getBytes(charset) ) for ((path, language, content) <- mainSources) { val path0 = dir / "src" / "main" / language / path os.write(path0, content, createFolders = true) } for ((path, language, content) <- testSources) { val path0 = dir / "src" / "test" / language / path os.write(path0, content, createFolders = true) } } } object MavenProject { implicit val monoid: ConfigMonoid[MavenProject] = ConfigMonoid.derive } final case class MavenModel( model: String, groupId: String, artifactId: String, version: String, dependencies: Seq[MavenLibraryDependency], plugins: Seq[MavenPlugin], resourceDirectories: Seq[String] ) { private def resourceNodes: NodeSeq = if (resourceDirectories.isEmpty) NodeSeq.Empty else { val resourceNodes = resourceDirectories.map { path => {path} } {resourceNodes} } def toXml: Elem = {model} {groupId} {artifactId} {version} {dependencies.map(_.toXml)} {resourceNodes} {plugins.map(_.toXml)} } final case class MavenLibraryDependency( groupId: String, artifactId: String, version: String, scope: MavenScopes ) { private val scopeParam = if scope == MavenScopes.Main then scala.xml.Null else {scope.name} def toXml: Elem = {groupId} {artifactId} {version} {scopeParam} } final case class MavenPlugin( groupId: String, artifactId: String, version: String, jdk: String, additionalNode: Elem ) { def toXml: Elem = {groupId} {artifactId} {version} {additionalNode} } ================================================ FILE: modules/cli/src/main/scala/scala/cli/exportCmd/MavenProjectDescriptor.scala ================================================ package scala.cli.exportCmd import dependency.{AnyDependency, NoAttributes, ScalaNameAttributes} import scala.annotation.unused import scala.build.errors.BuildException import scala.build.internal.Constants import scala.build.options.{BuildOptions, Scope, ShadowingSeq} import scala.build.{Logger, Positioned, Sources} import scala.cli.ScalaCli import scala.cli.exportCmd.POMBuilderHelper.* import scala.xml.Elem object POMBuilderHelper { def buildNode(name: String, value: String): Elem = new Elem( null, name, scala.xml.Null, scala.xml.TopScope, minimizeEmpty = false, scala.xml.Text(value) ) } final case class MavenProjectDescriptor( mavenPluginVersion: String, mavenScalaPluginVersion: String, mavenExecPluginVersion: String, extraSettings: Seq[String], mavenAppGroupId: String, mavenAppArtifactId: String, mavenAppVersion: String, logger: Logger ) extends ProjectDescriptor { private def sources(sourcesMain: Sources, sourcesTest: Sources): MavenProject = { val mainSources = ProjectDescriptor.sources(sourcesMain) val testSources = ProjectDescriptor.sources(sourcesTest) MavenProject( mainSources = mainSources, testSources = testSources ) } // todo: fill this - to be done in separate issue to reduce scope for maven export private def javaOptionsSettings(@unused options: BuildOptions): MavenProject = MavenProject( settings = Nil ) private def javacOptionsSettings(options: BuildOptions): List[String] = { val javacOptionsSettings = if (options.javaOptions.javacOptions.toSeq.isEmpty) Nil else { val options0 = options .javaOptions .javacOptions .toSeq .map(_.value) .map(o => "\"" + o.replace("\"", "\\\"") + "\"") options0 } javacOptionsSettings.toList } private def projectArtifactSettings( mavenAppGroupId: String, mavenAppArtifactId: String, mavenAppVersion: String ): MavenProject = MavenProject( groupId = Some(mavenAppGroupId), artifactId = Some(mavenAppArtifactId), version = Some(mavenAppVersion) ) private def dependencySettings( options: BuildOptions, testOptions: BuildOptions, scope: Scope, sources: Sources ): MavenProject = { val scalaV = getScalaVersion(options) def getScalaPrefix = if scalaV.startsWith("3") then "3" else if scalaV.startsWith("2.13") then "2.13" else "2.12" def buildMavenDepModels( mainDeps: ShadowingSeq[Positioned[AnyDependency]], isCompileOnly: Boolean ) = mainDeps.toSeq.toList.map(_.value).map { dep => val org = dep.organization val name = dep.name val ver = dep.version // TODO dep.userParams // TODO dep.exclude // TODO dep.attributes val artNameWithPrefix = dep.nameAttributes match { case NoAttributes => name case _: ScalaNameAttributes => s"${name}_$getScalaPrefix" } val scope0 = if (scope == Scope.Test) MavenScopes.Test else if (isCompileOnly) { System.err.println( s"Warning: Maven seems to support either test or provided, not both. So falling back to use Provided scope." ) MavenScopes.Provided } else MavenScopes.Main MavenLibraryDependency(org, artNameWithPrefix, ver, scope0) } val depSettings = { def toDependencies( mainDeps: ShadowingSeq[Positioned[AnyDependency]], testDeps: ShadowingSeq[Positioned[AnyDependency]], isCompileOnly: Boolean ): Seq[MavenLibraryDependency] = { val mainDependenciesMaven = buildMavenDepModels(mainDeps, isCompileOnly) val testDependenciesMaven = buildMavenDepModels(testDeps, isCompileOnly) val resolvedDeps = (mainDependenciesMaven ++ testDependenciesMaven).groupBy(k => k.groupId + k.artifactId + k.version ).map { (_, list) => val highestScope = MavenScopes.getHighestPriorityScope(list.map(_.scope)) list.head.copy(scope = highestScope) }.toList val scalaDep = if (!ProjectDescriptor.isPureJavaProject(options, sources)) { val scalaDep = if scalaV.startsWith("3") then "scala3-library_3" else "scala-library" val scalaCompilerDep = if scalaV.startsWith("3") then "scala3-compiler_3" else "scala-compiler" List( MavenLibraryDependency("org.scala-lang", scalaDep, scalaV, MavenScopes.Main), MavenLibraryDependency("org.scala-lang", scalaCompilerDep, scalaV, MavenScopes.Main) ) } else Nil resolvedDeps ++ scalaDep } toDependencies( options.classPathOptions.allExtraDependencies, testOptions.classPathOptions.allExtraDependencies, true ) } MavenProject( dependencies = depSettings ) } private def getScalaVersion(options: BuildOptions): String = options.scalaParams.toOption.flatten.map(_.scalaVersion).getOrElse( ScalaCli.getDefaultScalaVersion ) private def plugins( options: BuildOptions, jdkVersion: String, sourcesMain: Sources ): MavenProject = { val pureJava = ProjectDescriptor.isPureJavaProject(options, sourcesMain) val javacOptions = javacOptionsSettings(options) val mavenJavaPlugin = buildJavaCompilerPlugin(javacOptions, jdkVersion) val mavenExecPlugin = buildJavaExecPlugin(jdkVersion) val scalaPlugin = buildScalaPlugin(jdkVersion) val reqdPlugins = if (pureJava) Seq(mavenJavaPlugin, mavenExecPlugin) else Seq(mavenJavaPlugin, scalaPlugin) MavenProject( plugins = reqdPlugins ) } private def buildScalaPlugin(jdkVersion: String): MavenPlugin = { val execElements = compile testCompile MavenPlugin( "net.alchim31.maven", "scala-maven-plugin", mavenScalaPluginVersion, jdkVersion, execElements ) } private def buildJavaCompilerPlugin( javacOptions: Seq[String], jdkVersion: String ): MavenPlugin = { val javacOptionsElem = { val opts = javacOptions.map { opt => buildNode("arg", opt) } {opts} } val sourceArg = buildNode("source", jdkVersion) val targetArg = buildNode("target", jdkVersion) val configNode = {javacOptionsElem} {sourceArg} {targetArg} MavenPlugin( "org.apache.maven.plugins", "maven-compiler-plugin", mavenPluginVersion, jdkVersion, configNode ) } private def buildJavaExecPlugin(jdkVersion: String): MavenPlugin = MavenPlugin( "org.codehaus.mojo", "exec-maven-plugin", mavenExecPluginVersion, jdkVersion, ) private def customResourcesSettings(options: BuildOptions): MavenProject = { val resourceDirs = if (options.classPathOptions.resourcesDir.isEmpty) Nil else options.classPathOptions.resourcesDir.map(_.toNIO.toAbsolutePath.toString) MavenProject( resourceDirectories = resourceDirs ) } def `export`( optionsMain: BuildOptions, optionsTest: BuildOptions, sourcesMain: Sources, sourcesTest: Sources ): Either[BuildException, MavenProject] = { val jdk = optionsMain.javaOptions.jvmIdOpt.map(_.value) .getOrElse(Constants.defaultJavaVersion.toString) val projectChunks = Seq( sources(sourcesMain, sourcesTest), javaOptionsSettings(optionsMain), dependencySettings(optionsMain, optionsTest, Scope.Main, sourcesMain), customResourcesSettings(optionsMain), plugins(optionsMain, jdk, sourcesMain), projectArtifactSettings(mavenAppGroupId, mavenAppArtifactId, mavenAppVersion) ) Right(projectChunks.foldLeft(MavenProject())(_ + _)) } } enum MavenScopes(val priority: Int, val name: String) { case Main extends MavenScopes(1, "main") case Test extends MavenScopes(2, "test") case Provided extends MavenScopes(3, "provided") } object MavenScopes { def getHighestPriorityScope(scopes: Seq[MavenScopes]): MavenScopes = // if scope is empty return Main Scope, depending on priority, with 1 being highest scopes.minByOption(_.priority).getOrElse(Main) } ================================================ FILE: modules/cli/src/main/scala/scala/cli/exportCmd/MillProject.scala ================================================ package scala.cli.exportCmd import java.nio.charset.StandardCharsets import scala.build.options.ConfigMonoid import scala.cli.util.SeqHelpers.* import scala.reflect.NameTransformer import scala.util.Properties final case class MillProject( millVersion: Option[String] = None, mainDeps: Seq[String] = Nil, mainCompileOnlyDeps: Seq[String] = Nil, testDeps: Seq[String] = Nil, testCompileOnlyDeps: Seq[String] = Nil, scalaVersion: Option[String] = None, scalacOptions: Seq[String] = Nil, scalaCompilerPlugins: Seq[String] = Nil, scalaJsVersion: Option[String] = None, scalaNativeVersion: Option[String] = None, nameOpt: Option[String] = None, launchers: Seq[(os.RelPath, Array[Byte])] = Nil, mainSources: Seq[(os.SubPath, String, Array[Byte])] = Nil, testSources: Seq[(os.SubPath, String, Array[Byte])] = Nil, extraDecls: Seq[String] = Nil, resourcesDirs: Seq[os.Path] = Nil, extraTestDecls: Seq[String] = Nil, mainClass: Option[String] = None ) extends Project { private lazy val isMill1OrNewer: Boolean = !millVersion.exists(_.startsWith("0.")) def +(other: MillProject): MillProject = MillProject.monoid.orElse(this, other) private def name = nameOpt.getOrElse("project") def writeTo(dir: os.Path): Unit = { val nl = System.lineSeparator() val charSet = StandardCharsets.UTF_8 for ((relPath, content) <- launchers) { val dest = dir / relPath os.write(dest, content, createFolders = true) if (!Properties.isWin) os.perms.set(dest, "rwxr-xr-x") } for (ver <- millVersion) os.write(dir / ".mill-version", ver.getBytes(charSet), createFolders = true) val escapedName = if (NameTransformer.encode(name) == name) name else "`" + name + "`" val (parentModule, maybeExtraImport, maybePlatformVer) = if (scalaVersion.isEmpty) ("JavaModule", None, None) else scalaJsVersion .map(ver => ( "ScalaJSModule", Some("import mill.scalajslib._"), Some(s"""def scalaJSVersion = "$ver"""") ) ) .orElse( scalaNativeVersion.map(ver => ( "ScalaNativeModule", Some("import mill.scalanativelib._"), Some(s"""def scalaNativeVersion = "$ver"""") ) ) ) .getOrElse(("ScalaModule", None, None)) val maybeScalaVer = scalaVersion.map { sv => s"""def scalaVersion = "$sv"""" } val maybeScalacOptions = if (scalacOptions.isEmpty) None else { val optsString = scalacOptions.map(opt => s"\"$opt\"").mkString(", ") Some(s"""def scalacOptions = super.scalacOptions() ++ Seq($optsString)""") } def maybeDeps(deps: Seq[String], isCompileOnly: Boolean = false) = { val depsDefinition = isCompileOnly -> isMill1OrNewer match { case (true, true) => Seq("def compileMvnDeps = super.compileMvnDeps() ++ Seq(") case (true, false) => Seq("def compileIvyDeps = super.compileIvyDeps() ++ Seq(") case (false, true) => Seq("def mvnDeps = super.mvnDeps() ++ Seq(") case (false, false) => Seq("def ivyDeps = super.ivyDeps() ++ Seq(") } if deps.isEmpty then Seq.empty[String] else depsDefinition ++ deps .map { case dep if isMill1OrNewer => s""" mvn"$dep"""" case dep => s""" ivy"$dep"""" } .appendOnInit(",") ++ Seq(")") } val maybeScalaCompilerPlugins = if scalaCompilerPlugins.isEmpty then Seq.empty else Seq( if isMill1OrNewer then "def scalacPluginMvnDeps = super.scalacPluginMvnDeps() ++ Seq(" else "def scalacPluginIvyDeps = super.scalacPluginIvyDeps() ++ Seq(" ) ++ scalaCompilerPlugins .map { case dep if isMill1OrNewer => s" mvn\"$dep\"" case dep => s" ivy\"$dep\"" } .appendOnInit(",") ++ Seq(")") val maybeMain = mainClass.map { mc => s"""def mainClass = Some("$mc")""" } val customResourcesDecls = if (resourcesDirs.isEmpty) Nil else { val resources = resourcesDirs.map { case p if isMill1OrNewer => s"""mill.api.BuildCtx.workspaceRoot / os.RelPath("${p.relativeTo(dir)}")""" case p => s"""T.workspace / os.RelPath("${p.relativeTo(dir)}")""" } Seq("def runClasspath = super.runClasspath() ++ Seq(") ++ resources.map(resource => s" $resource").appendOnInit(",") ++ Seq(").map(PathRef(_))") } val millScalaTestPlatform = if (scalaJsVersion.nonEmpty) "ScalaJSTests" else if (scalaNativeVersion.nonEmpty) "ScalaNativeTests" else "ScalaTests" val maybeTestDefinition = if (testSources.nonEmpty) Seq( "", s" object test extends $millScalaTestPlatform {" ) ++ maybeDeps(testDeps).map(s => s" $s") ++ maybeDeps(testCompileOnlyDeps, isCompileOnly = true).map(s => s" $s") ++ extraTestDecls.map(s => s" $s") ++ Seq(" }") else Seq.empty val buildFileContent: String = { val parts: Seq[String] = Seq( "import mill._", "import mill.scalalib._" ) ++ maybeExtraImport ++ Seq( s"object $escapedName extends $parentModule {" ) ++ maybeScalaVer.map(s => s" $s") ++ maybePlatformVer.map(s => s" $s") ++ maybeScalacOptions.map(s => s" $s") ++ maybeDeps(mainDeps).map(s => s" $s") ++ maybeDeps(mainCompileOnlyDeps, isCompileOnly = true).map(s => s" $s") ++ maybeScalaCompilerPlugins.map(s => s" $s") ++ maybeMain.map(s => s" $s") ++ customResourcesDecls.map(s => s" $s") ++ extraDecls.map(" " + _) ++ maybeTestDefinition ++ Seq("}", "") parts.mkString(nl) } for ((path, _, content) <- mainSources) { val path0 = dir / name / "src" / path os.write(path0, content, createFolders = true) } for ((path, _, content) <- testSources) { val path0 = dir / name / "test" / "src" / path os.write(path0, content, createFolders = true) } val outputBuildFile = if isMill1OrNewer then dir / "build.mill" else dir / "build.sc" os.write(outputBuildFile, buildFileContent.getBytes(charSet)) } } object MillProject { implicit val monoid: ConfigMonoid[MillProject] = ConfigMonoid.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/exportCmd/MillProjectDescriptor.scala ================================================ package scala.cli.exportCmd import coursier.maven.MavenRepository import coursier.parse.RepositoryParser import java.nio.file.Path import scala.build.errors.BuildException import scala.build.internal.Constants import scala.build.internal.Runner.frameworkNames import scala.build.options.{BuildOptions, Platform, ScalaJsOptions, ScalaNativeOptions, Scope} import scala.build.testrunner.{AsmTestRunner, Logger as TestRunnerLogger} import scala.build.{Logger, Sources} import scala.cli.ScalaCli final case class MillProjectDescriptor( millVersion: String, projectName: Option[String] = None, launchers: Seq[(os.RelPath, Array[Byte])], logger: Logger ) extends ProjectDescriptor { private def sourcesSettings(mainSources: Sources, testSources: Sources): MillProject = { val mainSources0 = ProjectDescriptor.sources(mainSources) val testSources0 = ProjectDescriptor.sources(testSources) MillProject(mainSources = mainSources0, testSources = testSources0) } private def scalaVersionSettings(options: BuildOptions, sources: Sources): MillProject = { val pureJava = ProjectDescriptor.isPureJavaProject(options, sources) val sv = options.scalaParams .toOption .flatten .map(_.scalaVersion) .getOrElse(ScalaCli.getDefaultScalaVersion) if pureJava then MillProject() else MillProject(scalaVersion = Some(sv)) } private def scalaCompilerPlugins(buildOptions: BuildOptions): MillProject = MillProject(scalaCompilerPlugins = buildOptions.scalaOptions.compilerPlugins.toSeq.map(_.value.render) ) private def scalacOptionsSettings(buildOptions: BuildOptions): MillProject = MillProject(scalacOptions = buildOptions.scalaOptions.scalacOptions.toSeq.map(_.value.value)) private def scalaJsSettings(options: ScalaJsOptions): MillProject = { val scalaJsVersion = Some(options.version.getOrElse(Constants.scalaJsVersion)) val moduleKindDecls = if (options.moduleKindStr.isEmpty) Nil else Seq(s"""def moduleKind = ModuleKind.${options.moduleKind(logger)}""") MillProject( scalaJsVersion = scalaJsVersion, extraDecls = moduleKindDecls ) } private def scalaNativeSettings(options: ScalaNativeOptions): MillProject = MillProject(scalaNativeVersion = Some(options.finalVersion)) private def dependencySettings( mainOptions: BuildOptions, testOptions: BuildOptions ): MillProject = { val mainDeps = mainOptions.classPathOptions.extraDependencies.toSeq.map(_.value.render) val compileMainDeps = mainOptions.classPathOptions.extraCompileOnlyDependencies.toSeq.map(_.value.render) val testDeps = testOptions.classPathOptions.extraDependencies.toSeq.map(_.value.render) val compileTestDeps = testOptions.classPathOptions.extraCompileOnlyDependencies.toSeq.map(_.value.render) MillProject( mainDeps = mainDeps.toSeq, mainCompileOnlyDeps = compileMainDeps.toSeq, testDeps = testDeps.toSeq, testCompileOnlyDeps = compileTestDeps.toSeq ) } private def repositorySettings(options: BuildOptions): MillProject = { val repoDecls = if (options.classPathOptions.extraRepositories.isEmpty) Nil else { val repos = options.classPathOptions .extraRepositories .map(repo => RepositoryParser.repository(repo)) .map { case Right(repo: MavenRepository) => // TODO repo.authentication? s"""coursier.maven.MavenRepository("${repo.root}")""" case _ => ??? } Seq(s"""def repositories = super.repositories ++ Seq(${repos.mkString(", ")})""") } MillProject( extraDecls = repoDecls ) } private def customResourcesSettings(options: BuildOptions): MillProject = MillProject(resourcesDirs = options.classPathOptions.resourcesDir) private def customJarsSettings(options: BuildOptions): MillProject = { val customCompileOnlyJarsDecls = if options.classPathOptions.extraCompileOnlyJars.isEmpty then Nil else { val jars = options.classPathOptions.extraCompileOnlyJars.map(p => s"""PathRef(os.Path("$p"))""") Seq(s"""def compileClasspath = super.compileClasspath() ++ Seq(${jars.mkString(", ")})""") } val customJarsDecls = if options.classPathOptions.extraClassPath.isEmpty then Nil else { val jars = options.classPathOptions.extraClassPath.map(p => s"""PathRef(os.Path("$p"))""") Seq( s"""def unmanagedClasspath = super.unmanagedClasspath() ++ Seq(${jars.mkString(", ")})""" ) } MillProject(extraDecls = customCompileOnlyJarsDecls ++ customJarsDecls) } private def testFrameworkSettings(options: BuildOptions): MillProject = { val testClassPath: Seq[Path] = options.artifacts(logger, Scope.Test) match { case Right(artifacts) => artifacts.classPath.map(_.toNIO) case Left(exception) => logger.debug(exception.message) Seq.empty } val parentInspector = new AsmTestRunner.ParentInspector(testClassPath, TestRunnerLogger(logger.verbosity)) val frameworkName0 = options.testOptions.frameworks.headOption.orElse { frameworkNames(testClassPath, parentInspector, logger).toOption .flatMap(_.headOption) // TODO: handle multiple frameworks here } val testFrameworkDecls = frameworkName0 match { case None => Nil case Some(fw) => Seq(s"""def testFramework = "$fw"""") } MillProject( extraTestDecls = testFrameworkDecls ) } def `export`( optionsMain: BuildOptions, optionsTest: BuildOptions, sourcesMain: Sources, sourcesTest: Sources ): Either[BuildException, MillProject] = { // FIXME Put a sensible value in MillProject.nameOpt val baseSettings = MillProject( millVersion = Some(millVersion), nameOpt = projectName, launchers = launchers, mainClass = optionsMain.mainClass ) val settings = Seq( baseSettings, sourcesSettings(sourcesMain, sourcesTest), scalaVersionSettings(optionsMain, sourcesMain), scalacOptionsSettings(optionsMain), scalaCompilerPlugins(optionsMain), dependencySettings(optionsMain, optionsTest), repositorySettings(optionsMain), if optionsMain.platform.value == Platform.JS then scalaJsSettings(optionsMain.scalaJsOptions) else MillProject(), if optionsMain.platform.value == Platform.Native then scalaNativeSettings(optionsMain.scalaNativeOptions) else MillProject(), customResourcesSettings(optionsMain), customJarsSettings(optionsMain), testFrameworkSettings(optionsTest) ) Right(settings.foldLeft(MillProject())(_ + _)) } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/exportCmd/Project.scala ================================================ package scala.cli.exportCmd abstract class Project extends Product with Serializable { def writeTo(dir: os.Path): Unit } ================================================ FILE: modules/cli/src/main/scala/scala/cli/exportCmd/ProjectDescriptor.scala ================================================ package scala.cli.exportCmd import dependency.NoAttributes import scala.build.errors.BuildException import scala.build.options.{BuildOptions, ScalaJsOptions} import scala.build.{Logger, Sources} abstract class ProjectDescriptor extends Product with Serializable { def `export`( optionsMain: BuildOptions, optionsTest: BuildOptions, sourcesMain: Sources, sourcesTest: Sources ): Either[BuildException, Project] } object ProjectDescriptor { def sources(sources: Sources): Seq[(os.SubPath, String, Array[Byte])] = { val mainSources = sources.paths.map { case (path, relPath) => val language = if (path.last.endsWith(".java")) "java" else "scala" // FIXME Others // FIXME asSubPath might throw… Make it a SubPath earlier in the API? (relPath.asSubPath, language, os.read.bytes(path)) } val extraMainSources = sources.inMemory.map { inMemSource => val language = if (inMemSource.generatedRelPath.last.endsWith(".java")) "java" else "scala" ( inMemSource.generatedRelPath.asSubPath, language, inMemSource.content ) } mainSources ++ extraMainSources } def scalaJsLinkerCalls(options: ScalaJsOptions, logger: Logger): Seq[String] = { var calls = Seq.empty[String] calls = calls ++ { if (options.moduleKindStr.isEmpty) Nil else Seq(s""".withModuleKind(ModuleKind.${options.moduleKind(logger)})""") } for (checkIr <- options.checkIr) calls = calls :+ s".withCheckIR($checkIr)" val withOptimizer = options.fullOpt.getOrElse(false) calls = calls :+ s".withOptimizer($withOptimizer)" calls = calls :+ s".withClosureCompiler($withOptimizer)" calls = calls :+ s".withSourceMap(${options.emitSourceMaps})" calls } def isPureJavaProject(options: BuildOptions, sources: Sources): Boolean = !options.scalaOptions.addScalaLibrary.contains(true) && !options.scalaOptions.addScalaCompiler.contains(true) && sources.hasJava && !sources.hasScala && options.classPathOptions.allExtraDependencies.toSeq .forall(_.value.nameAttributes == NoAttributes) } ================================================ FILE: modules/cli/src/main/scala/scala/cli/exportCmd/SbtProject.scala ================================================ package scala.cli.exportCmd import java.nio.charset.StandardCharsets import scala.build.options.ConfigMonoid final case class SbtProject( plugins: Seq[String] = Nil, imports: Seq[String] = Nil, settings: Seq[Seq[String]] = Nil, sbtVersion: Option[String] = None, mainSources: Seq[(os.SubPath, String, Array[Byte])] = Nil, testSources: Seq[(os.SubPath, String, Array[Byte])] = Nil ) extends Project { def +(other: SbtProject): SbtProject = SbtProject.monoid.orElse(this, other) def writeTo(dir: os.Path): Unit = { val nl = System.lineSeparator() val charset = StandardCharsets.UTF_8 for (ver <- sbtVersion) { val buildPropsContent = s"sbt.version=$ver" + nl os.write( dir / "project" / "build.properties", buildPropsContent.getBytes(charset), createFolders = true ) } if (plugins.nonEmpty) { val pluginsSbtContent = plugins .map { p => s"addSbtPlugin($p)" + nl } .mkString os.write(dir / "project" / "plugins.sbt", pluginsSbtContent.getBytes(charset)) } val buildSbtImportsContent = imports.map(_ + nl).mkString val buildSbtSettingsContent = settings .filter(_.nonEmpty) .map { settings0 => settings0.map(s => s + nl).mkString + nl } .mkString val buildSbtContent = buildSbtImportsContent + buildSbtSettingsContent os.write(dir / "build.sbt", buildSbtContent.getBytes(charset)) for ((path, language, content) <- mainSources) { val path0 = dir / "src" / "main" / language / path os.write(path0, content, createFolders = true) } for ((path, language, content) <- testSources) { val path0 = dir / "src" / "test" / language / path os.write(path0, content, createFolders = true) } } } object SbtProject { implicit val monoid: ConfigMonoid[SbtProject] = ConfigMonoid.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/exportCmd/SbtProjectDescriptor.scala ================================================ package scala.cli.exportCmd import coursier.ivy.IvyRepository import coursier.maven.MavenRepository import coursier.parse.RepositoryParser import dependency.{AnyDependency, NoAttributes, ScalaNameAttributes} import java.nio.file.Path import scala.build.errors.BuildException import scala.build.internal.Runner.frameworkNames import scala.build.options.{ BuildOptions, Platform, ScalaJsOptions, ScalaNativeOptions, Scope, ShadowingSeq } import scala.build.testrunner.{AsmTestRunner, Logger as TestRunnerLogger} import scala.build.{Logger, Positioned, Sources} import scala.cli.ScalaCli final case class SbtProjectDescriptor( sbtVersion: String, extraSettings: Seq[String], logger: Logger ) extends ProjectDescriptor { private val q = "\"" private val nl = System.lineSeparator() private def sources(sourcesMain: Sources, sourcesTest: Sources): SbtProject = { val mainSources = ProjectDescriptor.sources(sourcesMain) val testSources = ProjectDescriptor.sources(sourcesTest) SbtProject( mainSources = mainSources, testSources = testSources ) } private def sbtVersionProject: SbtProject = SbtProject(sbtVersion = Some(sbtVersion)) private def pureJavaSettings(options: BuildOptions, sources: Sources): SbtProject = { val pureJava = ProjectDescriptor.isPureJavaProject(options, sources) val settings = if (pureJava) Seq( "crossPaths := false", "autoScalaLibrary := false" ) else if (options.scalaOptions.addScalaLibrary.getOrElse(true)) Nil else Seq( "autoScalaLibrary := false" ) SbtProject(settings = Seq(settings)) } private def scalaJsSettings(options: ScalaJsOptions): SbtProject = { val plugins = Seq( s""""org.scala-js" % "sbt-scalajs" % "${options.finalVersion}"""" ) val pluginSettings = Seq( "enablePlugins(ScalaJSPlugin)", "scalaJSUseMainModuleInitializer := true" ) val linkerConfigCalls = ProjectDescriptor.scalaJsLinkerCalls(options, logger) val linkerConfigSettings = if (linkerConfigCalls.isEmpty) Nil else Seq(s"""scalaJSLinkerConfig ~= { _${linkerConfigCalls.mkString} }""") // TODO options.dom SbtProject( plugins = plugins, settings = Seq(pluginSettings, linkerConfigSettings) ) } private def scalaNativeSettings(options: ScalaNativeOptions): SbtProject = { val plugins = Seq( s""""org.scala-native" % "sbt-scala-native" % "${options.finalVersion}"""" ) val pluginSettings = Seq( "enablePlugins(ScalaNativePlugin)" ) val configCalls = Seq.empty[String] val (configImports, configSettings) = if (configCalls.isEmpty) ("", Nil) else ( "import scala.scalanative.build._", Seq(s"""nativeConfig ~= { _${configCalls.mkString} }""") ) SbtProject( plugins = plugins, settings = Seq(pluginSettings, configSettings), imports = Seq(configImports) ) } private def scalaVersionSettings(options: BuildOptions): SbtProject = { val scalaVerSetting = { val sv = options.scalaParams.toOption.flatten.map(_.scalaVersion).getOrElse( ScalaCli.getDefaultScalaVersion ) s"""scalaVersion := "$sv"""" } SbtProject( settings = Seq(Seq(scalaVerSetting)) ) } private def repositorySettings(options: BuildOptions): SbtProject = { val repoSettings = if (options.classPathOptions.extraRepositories.isEmpty) Nil else { val repos = options.classPathOptions .extraRepositories .map(repo => (repo, RepositoryParser.repository(repo))) .zipWithIndex .map { case ((_, Right(repo: IvyRepository)), idx) => // TODO repo.authentication? // TODO repo.metadataPatternOpt s"""Resolver.url("repo-$idx") artifacts "${repo.pattern.string}"""" case ((_, Right(repo: MavenRepository)), idx) => // TODO repo.authentication? s""""repo-$idx" at "${repo.root}"""" case _ => ??? } Seq(s"""resolvers ++= Seq(${repos.mkString(", ")})""") } SbtProject( settings = Seq(repoSettings) ) } private def customResourcesSettings(options: BuildOptions): SbtProject = { val customResourceSettings = if (options.classPathOptions.resourcesDir.isEmpty) Nil else { val resources = options.classPathOptions.resourcesDir.map(p => s"""file("$p")""") Seq( s"""Compile / unmanagedResourceDirectories ++= Seq(${resources.mkString(", ")})""" ) } SbtProject( settings = Seq(customResourceSettings) ) } private def customJarsSettings(options: BuildOptions): SbtProject = { val customCompileOnlyJarsSettings = if (options.classPathOptions.extraCompileOnlyJars.isEmpty) Nil else { val jars = options.classPathOptions.extraCompileOnlyJars.map(p => s"""file("$p")""") Seq(s"""Compile / unmanagedClasspath ++= Seq(${jars.mkString(", ")})""") } val customJarsSettings = if (options.classPathOptions.extraClassPath.isEmpty) Nil else { val jars = options.classPathOptions.extraClassPath.map(p => s"""file("$p")""") Seq( s"""Compile / unmanagedClasspath ++= Seq(${jars.mkString(", ")})""", s"""Runtime / unmanagedClasspath ++= Seq(${jars.mkString(", ")})""" ) } SbtProject( settings = Seq(customCompileOnlyJarsSettings, customJarsSettings) ) } private def javaOptionsSettings(options: BuildOptions): SbtProject = { val javaOptionsSettings = if (options.javaOptions.javaOpts.toSeq.isEmpty) Nil else Seq( "run / javaOptions ++= Seq(" + nl + options.javaOptions .javaOpts .toSeq .map(_.value.value) .map { opt => " \"" + opt + "\"," + nl } .mkString + ")" ) SbtProject( settings = Seq(javaOptionsSettings) ) } private def mainClassSettings(options: BuildOptions): SbtProject = { val mainClassOptions = options.mainClass match { case None => Nil case Some(mainClass) => Seq(s"""Compile / mainClass := Some("$mainClass")""") } SbtProject( settings = Seq(mainClassOptions) ) } private def scalacOptionsSettings(options: BuildOptions): SbtProject = { val scalacOptionsSettings = if (options.scalaOptions.scalacOptions.toSeq.isEmpty) Nil else { val options0 = options .scalaOptions .scalacOptions .toSeq .map(_.value.value) .map(o => "\"" + o.replace("\"", "\\\"") + "\"") Seq(s"""scalacOptions ++= Seq(${options0.mkString(", ")})""") } SbtProject( settings = Seq(scalacOptionsSettings) ) } private def testFrameworkSettings(options: BuildOptions): SbtProject = { val testClassPath: Seq[Path] = options.artifacts(logger, Scope.Test) match { case Right(artifacts) => artifacts.classPath.map(_.toNIO) case Left(exception) => logger.debug(exception.message) Seq.empty } val parentInspector = new AsmTestRunner.ParentInspector(testClassPath, TestRunnerLogger(logger.verbosity)) val frameworkName0 = options.testOptions.frameworks.headOption.orElse { frameworkNames(testClassPath, parentInspector, logger).toOption .flatMap(_.headOption) // TODO: handle multiple frameworks here } val testFrameworkSettings = frameworkName0 match { case None => Nil case Some(fw) => Seq(s"""testFrameworks += new TestFramework("$fw")""") } SbtProject( settings = Seq(testFrameworkSettings) ) } private def dependencySettings(options: BuildOptions, scope: Scope): SbtProject = { val depSettings = { def toDepString(deps: ShadowingSeq[Positioned[AnyDependency]], isCompileOnly: Boolean) = deps.toSeq.toList.map(_.value).map { dep => val org = dep.organization val name = dep.name val ver = dep.version // TODO dep.userParams // TODO dep.exclude // TODO dep.attributes val (sep, suffixOpt) = dep.nameAttributes match { case NoAttributes => ("%", None) case s: ScalaNameAttributes => val suffixOpt0 = if (s.fullCrossVersion.getOrElse(false)) Some(".cross(CrossVersion.full)") else None val sep = "%%" (sep, suffixOpt0) } val scope0 = // FIXME This ignores the isCompileOnly when scope == Scope.Test if (scope == Scope.Test) "% Test" else if (isCompileOnly) "% Provided" else "" val baseDep = s"""$q$org$q $sep $q$name$q % $q$ver$q $scope0""" suffixOpt.fold(baseDep)(suffix => s"($baseDep)$suffix") } val allDepStrings = toDepString(options.classPathOptions.extraDependencies, false) ++ toDepString(options.classPathOptions.extraCompileOnlyDependencies, true) if (allDepStrings.isEmpty) Nil else if (allDepStrings.lengthCompare(1) == 0) Seq(s"""libraryDependencies += ${allDepStrings.head}""") else { val count = allDepStrings.length val allDeps = allDepStrings .iterator .zipWithIndex .map { case (dep, idx) => val maybeComma = if (idx == count - 1) "" else "," " " + dep + maybeComma + nl } .mkString Seq(s"""libraryDependencies ++= Seq($nl$allDeps)""") } } SbtProject( settings = Seq(depSettings) ) } def `export`( optionsMain: BuildOptions, optionsTest: BuildOptions, sourcesMain: Sources, sourcesTest: Sources ): Either[BuildException, SbtProject] = { // TODO Handle Scala CLI cross-builds val projectChunks = Seq( SbtProject(settings = Seq(extraSettings)), sources(sourcesMain, sourcesTest), sbtVersionProject, scalaVersionSettings(optionsMain), scalacOptionsSettings(optionsMain), mainClassSettings(optionsMain), pureJavaSettings(optionsMain, sourcesMain), javaOptionsSettings(optionsMain), if (optionsMain.platform.value == Platform.JS) scalaJsSettings(optionsMain.scalaJsOptions) else SbtProject(), if (optionsMain.platform.value == Platform.Native) scalaNativeSettings(optionsMain.scalaNativeOptions) else SbtProject(), customJarsSettings(optionsMain), customResourcesSettings(optionsMain), testFrameworkSettings(optionsTest), repositorySettings(optionsMain), dependencySettings(optionsMain, Scope.Main), dependencySettings(optionsTest, Scope.Test) ) Right(projectChunks.foldLeft(SbtProject())(_ + _)) } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/internal/Argv0.scala ================================================ package scala.cli.internal class Argv0 { def get(defaultValue: String): String = defaultValue } ================================================ FILE: modules/cli/src/main/scala/scala/cli/internal/CachedBinary.scala ================================================ package scala.cli.internal import java.math.BigInteger import java.nio.charset.StandardCharsets import java.security.MessageDigest import scala.build.Build import scala.build.input.{OnDisk, ResourceDirectory} import scala.build.internal.Constants object CachedBinary { final case class CacheData(changed: Boolean, projectSha: String) private def resolveProjectShaPath(workDir: os.Path) = workDir / ".project_sha" private def resolveOutputShaPath(workDir: os.Path) = workDir / ".output_sha" private def fileSha(filePath: os.Path): String = { val md = MessageDigest.getInstance("SHA-1") md.update(os.read.bytes(filePath)) val digest = md.digest() val calculatedSum = new BigInteger(1, digest) String.format(s"%040x", calculatedSum) } private def hashResources(build: Build.Successful) = { def hashResourceDir(path: os.Path) = os.walk(path) .filter(os.isFile(_)) .map { filePath => val md = MessageDigest.getInstance("SHA-1") md.update(os.read.bytes(filePath)) s"$filePath:" + new BigInteger(1, md.digest()).toString() } val classpathResourceDirsIt = build.options .classPathOptions .resourcesDir .flatMap(dir => hashResourceDir(dir)) .iterator ++ Iterator("\n") val projectResourceDirsIt = build.inputs.elements.iterator.flatMap { case elem: OnDisk => val content = elem match { case resDirInput: ResourceDirectory => hashResourceDir(resDirInput.path) case _ => List.empty } Iterator(elem.path.toString) ++ content.iterator ++ Iterator("\n") case _ => Iterator.empty } (classpathResourceDirsIt ++ projectResourceDirsIt) .map(_.getBytes(StandardCharsets.UTF_8)) } private def projectSha(builds: Seq[Build.Successful], config: List[String]): String = { val md = MessageDigest.getInstance("SHA-1") val charset = StandardCharsets.UTF_8 md.update(builds.map(_.inputs.sourceHash()).reduce(_ + _).getBytes(charset)) md.update("".getBytes()) // Resource changes for SN require relinking, so they should also be hashed builds.foreach(build => hashResources(build).foreach(md.update)) md.update("".getBytes()) md.update(0: Byte) md.update("".getBytes(charset)) for (elem <- config) { md.update(elem.getBytes(charset)) md.update(0: Byte) } md.update("".getBytes(charset)) md.update(Constants.version.getBytes) md.update(0: Byte) for (h <- builds.map(_.options).reduce(_.orElse(_)).hash) { md.update(h.getBytes(charset)) md.update(0: Byte) } val digest = md.digest() val calculatedSum = new BigInteger(1, digest) String.format(s"%040x", calculatedSum) } def updateProjectAndOutputSha( dest: os.Path, workDir: os.Path, currentProjectSha: String ): Unit = { val projectShaPath = resolveProjectShaPath(workDir) os.write.over(projectShaPath, currentProjectSha, createFolders = true) val outputShaPath = resolveOutputShaPath(workDir) val sha = fileSha(dest) os.write.over(outputShaPath, sha) } def getCacheData( builds: Seq[Build.Successful], config: List[String], dest: os.Path, workDir: os.Path ): CacheData = { val projectShaPath = resolveProjectShaPath(workDir) val outputShaPath = resolveOutputShaPath(workDir) val currentProjectSha = projectSha(builds, config) val currentOutputSha = if os.exists(dest) then Some(fileSha(dest)) else None val previousProjectSha = if os.exists(projectShaPath) then Some(os.read(projectShaPath)) else None val previousOutputSha = if os.exists(outputShaPath) then Some(os.read(outputShaPath)) else None val changed = !previousProjectSha.contains(currentProjectSha) || previousOutputSha != currentOutputSha || !os.exists(dest) CacheData(changed, currentProjectSha) } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/internal/CliLogger.scala ================================================ package scala.cli.internal import bloop.rifle.BloopRifleLogger import ch.epfl.scala.bsp4j as b import coursier.cache.CacheLogger import coursier.cache.loggers.{FallbackRefreshDisplay, RefreshLogger} import org.scalajs.logging.{Level as ScalaJsLevel, Logger as ScalaJsLogger, ScalaConsoleLogger} import java.io.PrintStream import scala.build.bsp.protocol.TextEdit import scala.build.errors.{BuildException, CompositeBuildException, Diagnostic, Severity} import scala.build.internal.CustomProgressBarRefreshDisplay import scala.build.internal.util.WarningMessages import scala.build.internals.FeatureType import scala.build.{ConsoleBloopBuildClient, Logger, Position} import scala.collection.mutable import scala.scalanative.build as sn class CliLogger( val verbosity: Int, quiet: Boolean, progress: Option[Boolean], out: PrintStream ) extends Logger { logger => override def log(diagnostics: Seq[Diagnostic]): Unit = { val hashMap = new mutable.HashMap[os.Path, Seq[String]] diagnostics.foreach { d => printDiagnostic( d.positions, d.severity, d.message, hashMap, d.textEdit ) } } def error(message: String) = out.println(message) def message(message: => String) = if (verbosity >= 0) out.println(message) def log(message: => String) = if (verbosity >= 1) out.println(message) def log(message: => String, debugMessage: => String) = if (verbosity >= 2) out.println(debugMessage) else if (verbosity >= 1) out.println(message) def debug(message: => String) = if (verbosity >= 2) out.println(message) def printDiagnostic( positions: Seq[Position], severity: Severity, message: String, contentCache: mutable.Map[os.Path, Seq[String]], textEditOpt: Option[Diagnostic.TextEdit] ) = if (positions.isEmpty) out.println( s"${ConsoleBloopBuildClient.diagnosticPrefix(severity)} $message" ) else { val positions0 = positions.distinct val filePositions = positions0.collect { case f: Position.File => f } val otherPositions = positions0.filter { case _: Position.File => false case _ => true } for (f <- filePositions) { val startPos = new b.Position(f.startPos._1, f.startPos._2) val endPos = new b.Position(f.endPos._1, f.endPos._2) val range = new b.Range(startPos, endPos) val diag = new b.Diagnostic(range, message) diag.setSeverity(severity.toBsp4j) diag.setSource("scala-cli") for (textEdit <- textEditOpt) { val bTextEdit = TextEdit(range, textEdit.newText) diag.setData(bTextEdit.toJsonTree()) } for (file <- f.path) { val lines = contentCache.getOrElseUpdate(file, os.read(file).linesIterator.toVector) if (f.startPos._1 < lines.length) diag.setCode(lines(f.startPos._1)) } ConsoleBloopBuildClient.printFileDiagnostic( this, f.path, diag ) } if (otherPositions.nonEmpty) ConsoleBloopBuildClient.printOtherDiagnostic( this, message, severity, otherPositions ) } private def printEx( ex: BuildException, contentCache: mutable.Map[os.Path, Seq[String]] ): Unit = ex match { case c: CompositeBuildException => // FIXME We might want to order things here… Or maybe just collect all b.Diagnostics // below, and order them before printing them. for (ex <- c.exceptions) printEx(ex, contentCache) case _ => printDiagnostic(ex.positions, Severity.Error, ex.getMessage(), contentCache, None) } def log(ex: BuildException): Unit = if (verbosity >= 0) printEx(ex, new mutable.HashMap) def debug(ex: BuildException): Unit = if (verbosity >= 2) printEx(ex, new mutable.HashMap) def exit(ex: BuildException): Nothing = flushExperimentalWarnings if (verbosity < 0) sys.exit(1) else if (verbosity == 0) { printEx(ex, new mutable.HashMap) sys.exit(1) } else throw new Exception(ex) def coursierLogger(printBefore: String) = if (quiet) CacheLogger.nop else if (progress.getOrElse(coursier.paths.Util.useAnsiOutput())) RefreshLogger.create( CustomProgressBarRefreshDisplay.create( keepOnScreen = verbosity >= 1, if (printBefore.nonEmpty) System.err.println(printBefore), () ) ) else RefreshLogger.create(new FallbackRefreshDisplay) def bloopRifleLogger = new BloopRifleLogger { def info(msg: => String) = logger.message(msg) def debug(msg: => String) = if (verbosity >= 3) logger.debug(msg) def debug(msg: => String, ex: Throwable) = if (verbosity >= 3) { logger.debug(msg) if (ex != null) ex.printStackTrace(out) } def error(msg: => String, ex: Throwable) = { logger.error(s"Error: $msg ($ex)") if (verbosity >= 1 && ex != null) ex.printStackTrace(out) } def error(msg: => String) = logger.error(msg) def bloopBspStdout = if (verbosity >= 2) Some(out) else None def bloopBspStderr = if (verbosity >= 2) Some(out) else None def bloopCliInheritStdout = verbosity >= 3 def bloopCliInheritStderr = verbosity >= 3 } def scalaJsLogger: ScalaJsLogger = // FIXME Doesn't use 'out' new ScalaConsoleLogger( minLevel = if (verbosity >= 2) ScalaJsLevel.Debug else if (verbosity >= 1) ScalaJsLevel.Info else if (verbosity >= 0) ScalaJsLevel.Warn else ScalaJsLevel.Error ) def scalaNativeTestLogger: sn.Logger = new sn.Logger { def trace(msg: Throwable) = () def debug(msg: String) = logger.debug(msg) def info(msg: String) = logger.log(msg) def warn(msg: String) = logger.log(msg) def error(msg: String) = logger.message(msg) } val scalaNativeCliInternalLoggerOptions: List[String] = if (verbosity >= 1) List("-v", "-v", "-v") // debug else if (verbosity >= 0) List("-v", "-v") // info else List() // error // Allow to disable that? def compilerOutputStream = out private var experimentalWarnings: Map[FeatureType, Set[String]] = Map.empty private var reported: Map[FeatureType, Set[String]] = Map.empty def experimentalWarning(featureName: String, featureType: FeatureType): Unit = if (!reported.get(featureType).exists(_.contains(featureName))) experimentalWarnings ++= experimentalWarnings.updatedWith(featureType) { case None => Some(Set(featureName)) case Some(namesSet) => Some(namesSet + featureName) } def flushExperimentalWarnings: Unit = if (experimentalWarnings.nonEmpty) { val messageStr: String = { val namesAndTypes = for { (featureType, names) <- experimentalWarnings.toSeq.sortBy(_._1) // by feature type name <- names } yield name -> featureType WarningMessages.experimentalFeaturesUsed(namesAndTypes) } message(messageStr) reported = for { (featureType, names) <- experimentalWarnings reportedNames = reported.getOrElse(featureType, Set.empty[String]) } yield featureType -> (names ++ reportedNames) experimentalWarnings = Map.empty } override def cliFriendlyDiagnostic( message: String, cliFriendlyMessage: String, severity: Severity, positions: Seq[Position] ): Unit = diagnostic(cliFriendlyMessage, severity, Nil) } object CliLogger { def default: CliLogger = new CliLogger(0, false, None, System.err) } ================================================ FILE: modules/cli/src/main/scala/scala/cli/internal/PPrintStringPrefixHelper.scala ================================================ package scala.cli.internal // Remove once we can use https://github.com/com-lihaoyi/PPrint/pull/80 final class PPrintStringPrefixHelper { def apply(i: Iterable[Object]): String = i.collectionClassName } ================================================ FILE: modules/cli/src/main/scala/scala/cli/internal/Pid.scala ================================================ package scala.cli.internal import java.lang.management.ManagementFactory class Pid { def get(): Integer = try { val pid = ManagementFactory.getRuntimeMXBean.getName.takeWhile(_ != '@').toInt pid: Integer } catch { case _: NumberFormatException => null } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/internal/ProcUtil.scala ================================================ package scala.cli.internal import java.nio.charset.StandardCharsets import java.util.concurrent.{CancellationException, CompletableFuture, CompletionException} import scala.build.Logger import scala.build.internals.EnvVar import scala.util.Properties import scala.util.control.NonFatal object ProcUtil { def maybeUpdatePreamble(file: os.Path): Boolean = { val header = os.read.bytes(file, offset = 0, count = "#!/usr/bin/env sh".length).toSeq val hasBinEnvShHeader = header.startsWith("#!/usr/bin/env sh".getBytes(StandardCharsets.UTF_8)) val hasBinShHeader = header.startsWith("#!/bin/sh".getBytes(StandardCharsets.UTF_8)) val usesSh = hasBinEnvShHeader || hasBinShHeader if (usesSh) { val content = os.read.bytes(file) val updatedContent = if (hasBinEnvShHeader) "#!/usr/bin/env bash".getBytes(StandardCharsets.UTF_8) ++ content.drop("#!/usr/bin/env sh".length) else if (hasBinShHeader) "#!/bin/bash".getBytes(StandardCharsets.UTF_8) ++ content.drop("#!/bin/sh".length) else sys.error("Can't happen") os.write.over(file, updatedContent, createFolders = true) } usesSh } def forceKillProcess(process: Process, logger: Logger): Unit = { if (process.isAlive) { process.destroyForcibly() logger.debug(s"Killing user process ${process.pid()}") } } def interruptProcess(process: Process, logger: Logger): Unit = { val pid = process.pid() try if (process.isAlive) { logger.debug("Interrupting running process") if (Properties.isWin) { os.proc("taskkill", "/PID", pid).call() logger.debug(s"Run following command to interrupt process: 'taskkill /PID $pid'") } else { os.proc("kill", "-2", pid).call() logger.debug(s"Run following command to interrupt process: 'kill -2 $pid'") } } catch { // ignore the failure if the process isn't running, might mean it exited between the first check and the call of the command to kill it case NonFatal(e) => logger.debug(s"Ignoring error during interrupt process: $e") } } def waitForProcess(process: Process, onExit: CompletableFuture[?]): Unit = { process.waitFor() try onExit.join() catch { case _: CancellationException | _: CompletionException => // ignored } } def findApplicationPathsOnPATH(appName: String): List[String] = { import java.io.File.pathSeparator, java.io.File.pathSeparatorChar var path: String = EnvVar.Misc.path.valueOpt.getOrElse("") val pwd: String = os.pwd.toString // on unix & macs, an empty PATH counts as ".", the working directory if (path.length == 0) path = pwd else { // scala 'split' doesn't handle leading or trailing pathSeparators // correctly so expand them now. if (path.head == pathSeparatorChar) path = pwd + path if (path.last == pathSeparatorChar) path = path + pwd // on unix and macs, an empty PATH item is like "." (current dir). path = s"$pathSeparator$pathSeparator".r .replaceAllIn(path, pathSeparator + pwd + pathSeparator) } val appPaths = path .split(pathSeparator) .map(d => if (d == ".") pwd else d) // on unix a bare "." counts as the current dir .map(_ + s"/$appName") .filter(f => os.isFile(os.Path(f, os.pwd))) .toSet appPaths.toList } // Copied from https://github.com/scalacenter/bloop/blob/a249e0a710ce169ca05d0606778f96f44a398680/shared/src/main/scala/bloop/io/Environment.scala private lazy val shebangCapableShells = Seq( "/bin/sh", "/bin/ash", "/bin/bash", "/bin/dash", "/bin/mksh", "/bin/pdksh", "/bin/posh", "/bin/tcsh", "/bin/zsh", "/bin/fish" ) def isShebangCapableShell = EnvVar.Misc.shell.valueOpt match case Some(currentShell) if shebangCapableShells.exists(sh => currentShell.contains(sh)) => true case _ => false } ================================================ FILE: modules/cli/src/main/scala/scala/cli/internal/ProfileFileUpdater.scala ================================================ package scala.cli.internal import java.nio.charset.Charset import java.nio.file.{FileAlreadyExistsException, Files, Path} // initially adapted from https://github.com/coursier/coursier/blob/d9a0fcc1af4876bec7f19a18f2c93d808e06df8d/modules/env/src/main/scala/coursier/env/ProfileUpdater.scala#L44-L137 object ProfileFileUpdater { private def startEndIndices(start: String, end: String, content: String): Option[(Int, Int)] = { val startIdx = content.indexOf(start) if (startIdx >= 0) { val endIdx = content.indexOf(end, startIdx + 1) if (endIdx >= 0) Some(startIdx, endIdx + end.length) else None } else None } def addToProfileFile( file: Path, title: String, addition: String, charset: Charset ): Boolean = { def updated(content: String): Option[String] = { val start = s"# >>> $title >>>\n" val endStr = s"# <<< $title <<<\n" val withTags = "\n" + start + addition.stripSuffix("\n") + "\n" + endStr if (content.contains(withTags)) None else Some { startEndIndices(start, endStr, content) match { case None => content + withTags case Some((startIdx, endIdx)) => content.take(startIdx) + withTags + content.drop(endIdx) } } } var updatedSomething = false val contentOpt = Some(file) .filter(Files.exists(_)) .map(f => new String(Files.readAllBytes(f), charset)) for (updatedContent <- updated(contentOpt.getOrElse(""))) { Option(file.getParent).map(createDirectories(_)) Files.write(file, updatedContent.getBytes(charset)) updatedSomething = true } updatedSomething } def removeFromProfileFile( file: Path, title: String, charset: Charset ): Boolean = { def updated(content: String): Option[String] = { val start = s"# >>> $title >>>\n" val end = s"# <<< $title <<<\n" startEndIndices(start, end, content).map { case (startIdx, endIdx) => content.take(startIdx).stripSuffix("\n") + content.drop(endIdx) } } var updatedSomething = false val contentOpt = Some(file) .filter(Files.exists(_)) .map(f => new String(Files.readAllBytes(f), charset)) for (updatedContent <- updated(contentOpt.getOrElse(""))) { Option(file.getParent).map(createDirectories(_)) Files.write(file, updatedContent.getBytes(charset)) updatedSomething = true } updatedSomething } private def createDirectories(path: Path): Unit = try Files.createDirectories(path) catch { // Ignored, see https://bugs.openjdk.java.net/browse/JDK-8130464 case _: FileAlreadyExistsException if Files.isDirectory(path) => } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/internal/ScalaJsLinker.scala ================================================ package scala.cli.internal import coursier.VersionConstraint import coursier.cache.{ArchiveCache, FileCache} import coursier.util.Task import dependency.* import org.scalajs.testing.adapter.TestAdapterInitializer as TAI import java.io.{File, InputStream, OutputStream} import scala.build.EitherCps.{either, value} import scala.build.errors.{BuildException, ScalaJsLinkingError} import scala.build.internal.Util.{DependencyOps, ModuleOps} import scala.build.internal.{ExternalBinaryParams, FetchExternalBinary, Runner, ScalaJsLinkerConfig} import scala.build.options.scalajs.ScalaJsLinkerOptions import scala.build.{Logger, Positioned, RepositoryUtils} import scala.io.Source import scala.util.Properties object ScalaJsLinker { case class LinkJSInput( options: ScalaJsLinkerOptions, javaCommand: String, classPath: Seq[os.Path], mainClassOrNull: String, addTestInitializer: Boolean, config: ScalaJsLinkerConfig, fullOpt: Boolean, noOpt: Boolean, scalaJsVersion: String ) private def linkerMainClass = "org.scalajs.cli.Scalajsld" private def linkerCommand( options: ScalaJsLinkerOptions, javaCommand: String, logger: Logger, cache: FileCache[Task], archiveCache: ArchiveCache[Task], scalaJsVersion: String ): Either[BuildException, Seq[String]] = either { options.linkerPath match { case Some(path) => Seq(path.toString) case None => val scalaJsCliVersion = options.finalScalaJsCliVersion val scalaJsCliDep = { val mod = mod"org.virtuslab.scala-cli:scalajscli_2.13" dependency.Dependency(mod, s"$scalaJsCliVersion+") } val forcedVersions = Seq( mod"org.scala-js:scalajs-linker_2.13" -> scalaJsVersion ) val extraRepos = if scalaJsVersion.endsWith("SNAPSHOT") || scalaJsCliVersion.endsWith("SNAPSHOT") then Seq( RepositoryUtils.snapshotsRepository, RepositoryUtils.scala3NightlyRepository ) else Nil options.finalUseJvm match { case Right(()) => val (_, linkerRes) = value { scala.build.Artifacts.fetchCsDependencies( dependencies = Seq(Positioned.none(scalaJsCliDep.toCs)), extraRepositories = extraRepos, forceScalaVersionOpt = None, forcedVersions = forcedVersions.map { case (m, v) => (m.toCs, VersionConstraint(v)) }, logger = logger, cache = cache, classifiersOpt = None ) } val linkerClassPath = linkerRes.files val command = Seq[os.Shellable]( javaCommand, options.javaArgs, "-cp", linkerClassPath.map(_.getAbsolutePath).mkString(File.pathSeparator), linkerMainClass ) command.flatMap(_.value) case Left(osArch) => val useLatest = scalaJsVersion == "latest" val ext = if (Properties.isWin) ".zip" else ".gz" val tag = if (useLatest) "launchers" else s"v$scalaJsCliVersion" val url = s"https://github.com/virtusLab/scala-js-cli/releases/download/$tag/scala-js-ld-$osArch$ext" val params = ExternalBinaryParams( url, useLatest, "scala-js-ld", Seq(scalaJsCliDep), linkerMainClass, forcedVersions = forcedVersions, extraRepos = extraRepos ) val binary = value { FetchExternalBinary.fetch(params, archiveCache, logger, () => javaCommand) } binary.command } } } private def getCommand( input: LinkJSInput, linkingDir: os.Path, logger: Logger, cache: FileCache[Task], archiveCache: ArchiveCache[Task], useLongRunning: Boolean ) = either { val command = value { linkerCommand( input.options, input.javaCommand, logger, cache, archiveCache, input.scalaJsVersion ) } val allArgs = { val outputArgs = Seq("--outputDir", linkingDir.toString) val longRunning = if (useLongRunning) Seq("--longRunning") else Seq.empty[String] val mainClassArgs = Option(input.mainClassOrNull).toSeq.flatMap(mainClass => Seq("--mainMethod", mainClass + ".main") ) val testInitializerArgs = if (input.addTestInitializer) Seq("--mainMethodWithNoArgs", TAI.ModuleClassName + "." + TAI.MainMethodName) else Nil val optArg = if (input.noOpt) "--noOpt" else if (input.fullOpt) "--fullOpt" else "--fastOpt" Seq[os.Shellable]( outputArgs, mainClassArgs, testInitializerArgs, optArg, input.config.linkerCliArgs, input.classPath.map(_.toString), longRunning ) } command ++ allArgs.flatMap(_.value) } def link( input: LinkJSInput, linkingDir: os.Path, logger: Logger, cache: FileCache[Task], archiveCache: ArchiveCache[Task] ): Either[BuildException, Unit] = either { val useLongRunning = !input.fullOpt if (useLongRunning) longRunningProcess.startOrReuse(input, linkingDir, logger, cache, archiveCache) else { val cmd = value(getCommand(input, linkingDir, logger, cache, archiveCache, useLongRunning = false)) val res = Runner.run(cmd, logger) val retCode = res.waitFor() if (retCode == 0) logger.debug("Scala.js linker ran successfully") else { logger.debug(s"Scala.js linker exited with return code $retCode") value(Left(new ScalaJsLinkingError)) } } } private object longRunningProcess { case class Proc(process: Process, stdin: OutputStream, stdout: InputStream) { val stdoutLineIterator: Iterator[String] = Source.fromInputStream(stdout).getLines() } case class Input(input: LinkJSInput, linkingDir: os.Path) var currentInput: Option[Input] = None var currentProc: Option[Proc] = None def startOrReuse( linkJsInput: LinkJSInput, linkingDir: os.Path, logger: Logger, cache: FileCache[Task], archiveCache: ArchiveCache[Task] ) = either { val input = Input(linkJsInput, linkingDir) def createProcess(): Proc = { val cmd = value(getCommand( linkJsInput, linkingDir, logger, cache, archiveCache, useLongRunning = true )) val process = Runner.run(cmd, logger, inheritStreams = false) val stdin = process.getOutputStream() val stdout = process.getInputStream() val proc = Proc(process, stdin, stdout) currentProc = Some(proc) currentInput = Some(input) proc } def loop(proc: Proc): Unit = if (proc.stdoutLineIterator.hasNext) { val line = proc.stdoutLineIterator.next() if (line == "SCALA_JS_LINKING_DONE") logger.debug("Scala.js linker ran successfully") else { // inherit other stdout from Scala.js println(line) loop(proc) } } else { val retCode = proc.process.waitFor() logger.debug(s"Scala.js linker exited with return code $retCode") value(Left(new ScalaJsLinkingError)) } val proc = currentProc match { case Some(proc) if currentInput.contains(input) && proc.process.isAlive() => // trigger new linking proc.stdin.write('\n') proc.stdin.flush() proc case Some(proc) => proc.stdin.close() proc.stdout.close() proc.process.destroy() createProcess() case _ => createProcess() } loop(proc) } } def updateSourceMappingURL(mainJsPath: os.Path) = val content = os.read(mainJsPath) content.replace( "//# sourceMappingURL=main.js.map", s"//# sourceMappingURL=${mainJsPath.last}.map" ) } ================================================ FILE: modules/cli/src/main/scala/scala/cli/javaLauncher/JavaLauncherCli.scala ================================================ package scala.cli.javaLauncher import java.io.File import scala.build.Positioned import scala.build.internal.{OsLibc, Runner} import scala.build.options.{BuildOptions, JavaOptions} import scala.cli.commands.shared.LoggingOptions import scala.cli.javaLauncher.JavaLauncherCli.LauncherKind.* object JavaLauncherCli { def runAndExit(remainingArgs: Seq[String]): Nothing = { val logger = LoggingOptions().logger val scalaCliPath = System.getProperty("java.class.path").split(File.pathSeparator).iterator.toList.map { f => os.Path(f, os.pwd) } val buildOptions = BuildOptions( javaOptions = JavaOptions( jvmIdOpt = Some(OsLibc.defaultJvm(OsLibc.jvmIndexOs)).map(Positioned.none) ) ) val launcherKind = sys.props.get("scala-cli.kind") match { case Some("jvm.bootstrapped") => Bootstrapped case Some("jvm.standaloneLauncher") => StandaloneLauncher case _ => sys.error("should not happen") } val classPath = launcherKind match { case Bootstrapped => scalaCliPath case StandaloneLauncher => scalaCliPath.headOption.toList } val mainClass = launcherKind match { case Bootstrapped => "scala.cli.ScalaCli" case StandaloneLauncher => "coursier.bootstrap.launcher.ResourcesLauncher" } val exitCode = Runner.runJvm( buildOptions.javaHome().value.javaCommand, buildOptions.javaOptions.javaOpts.toSeq.map(_.value.value), classPath, mainClass, remainingArgs, logger, allowExecve = true ).waitFor() sys.exit(exitCode) } enum LauncherKind { case Bootstrapped, StandaloneLauncher } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/launcher/LauncherCli.scala ================================================ package scala.cli.launcher import coursier.Repositories import coursier.cache.FileCache import coursier.util.{Artifact, Task} import coursier.version.Version import dependency.* import scala.build.EitherCps.{either, value} import scala.build.errors.BuildException import scala.build.internal.CsLoggerUtil.CsCacheExtensions import scala.build.internal.Util.safeFullDetailedArtifacts import scala.build.internal.{Constants, OsLibc, Runner} import scala.build.options.ScalaVersionUtil.fileWithTtl0 import scala.build.options.{BuildOptions, JavaOptions} import scala.build.{Artifacts, Os, Positioned, RepositoryUtils} import scala.cli.ScalaCli import scala.cli.commands.shared.{CoursierOptions, LoggingOptions} import scala.xml.XML object LauncherCli { def runAndExit( version: String, options: LauncherOptions, remainingArgs: Seq[String] ): Either[BuildException, Nothing] = either { val logger = LoggingOptions().logger val cache = CoursierOptions().coursierCache(logger) val scalaVersion = options.cliScalaVersion.getOrElse(scalaCliScalaVersion(version)) val scalaParameters = ScalaParameters(scalaVersion) val snapshotsRepo = Seq( Repositories.central, RepositoryUtils.snapshotsRepository, RepositoryUtils.scala3NightlyRepository ) val cliVersion: String = if version == "nightly" then resolveNightlyScalaCliVersion(cache, scalaParameters.scalaBinaryVersion) else version val scalaCliDependency = Seq(dep"org.virtuslab.scala-cli::cli:$cliVersion") val fetchedScalaCli = Artifacts.fetchAnyDependencies( dependencies = scalaCliDependency.map(Positioned.none), extraRepositories = snapshotsRepo, paramsOpt = Some(scalaParameters), logger = logger, cache = cache.withMessage(s"Fetching ${ScalaCli.fullRunnerName} $cliVersion"), classifiersOpt = None ) match { case Right(value) => value case Left(value) => System.err.println(value.message) sys.exit(1) } val scalaCli: Seq[os.Path] = value(fetchedScalaCli.fullDetailedArtifacts0.safeFullDetailedArtifacts) .collect { case (_, _, _, Some(f)) => os.Path(f, os.pwd) } val buildOptions = BuildOptions( javaOptions = JavaOptions( jvmIdOpt = Some(OsLibc.defaultJvm(OsLibc.jvmIndexOs)).map(Positioned.none) ) ) val exitCode = Runner.runJvm( javaCommand = buildOptions.javaHome().value.javaCommand, javaArgs = buildOptions.javaOptions.javaOpts.toSeq.map(_.value.value), classPath = scalaCli, mainClass = "scala.cli.ScalaCli", args = remainingArgs, logger = logger, allowExecve = true ).waitFor() sys.exit(exitCode) } def scalaCliScalaVersion(cliVersion: String): String = if cliVersion == "nightly" then Constants.defaultScalaVersion else if Version(cliVersion) <= Version("0.1.2") then Constants.defaultScala212Version else if Version(cliVersion) <= Version("0.1.4") then Constants.defaultScala213Version else Constants.defaultScalaVersion def resolveNightlyScalaCliVersion( cache: FileCache[Task], scalaBinaryVersion: String ): String = { val cliSubPath = s"org/virtuslab/scala-cli/cli_$scalaBinaryVersion" val mavenMetadataUrl = s"${RepositoryUtils.snapshotsRepositoryUrl}/$cliSubPath/maven-metadata.xml" val artifact = Artifact(mavenMetadataUrl).withChanging(true) cache.fileWithTtl0(artifact) match { case Left(_) => System.err.println(s"Unable to find nightly ${ScalaCli.fullRunnerName} version") sys.exit(1) case Right(mavenMetadataXml) => val metadataXmlContent = os.read(os.Path(mavenMetadataXml, Os.pwd)) val parsed = XML.loadString(metadataXmlContent) val rawVersions = (parsed \ "versioning" \ "versions" \ "version").map(_.text) val versions = rawVersions.map(Version(_)) if versions.isEmpty then sys.error(s"No versions found in $mavenMetadataUrl (locally at $mavenMetadataXml)") else versions.max.repr } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/launcher/LauncherOptions.scala ================================================ package scala.cli.launcher import caseapp.* import com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker import scala.cli.commands.shared.HelpGroup import scala.cli.commands.tags @HelpMessage("Run another Scala CLI version") final case class LauncherOptions( @Group(HelpGroup.Launcher.toString) @HelpMessage("Set the Scala CLI version") @ValueDescription("nightly|version") @Tag(tags.implementation) @Tag(tags.inShortHelp) cliVersion: Option[String] = None, @Group(HelpGroup.Launcher.toString) @HelpMessage("The version of Scala on which Scala CLI was published") @ValueDescription("2.12|2.13|3") @Hidden @Tag(tags.implementation) cliScalaVersion: Option[String] = None, @Recurse scalaRunner: ScalaRunnerLauncherOptions = ScalaRunnerLauncherOptions(), @Recurse powerOptions: PowerOptions = PowerOptions() ) { def toCliArgs: List[String] = cliVersion.toList.flatMap(v => List("--cli-version", v)) ++ cliScalaVersion.toList.flatMap(v => List("--cli-scala-version", v)) ++ scalaRunner.toCliArgs ++ powerOptions.toCliArgs } object LauncherOptions { implicit lazy val parser: Parser[LauncherOptions] = Parser.derive implicit lazy val help: Help[LauncherOptions] = Help.derive implicit lazy val jsonCodec: JsonValueCodec[LauncherOptions] = JsonCodecMaker.make } ================================================ FILE: modules/cli/src/main/scala/scala/cli/launcher/PowerOptions.scala ================================================ package scala.cli.launcher import caseapp.* import scala.cli.commands.shared.HelpGroup import scala.cli.commands.tags /** Options extracted from [[LauncherOptions]] to allow for parsing them separately. Thanks to this * and additional parsing we can read the --power flag placed anywhere in the command invocation. * * This option is duplicated in [[scala.cli.commands.shared.GlobalOptions]] so that we can ensure * that no subcommand defines its own --power option Checking for clashing names is done in unit * tests. */ case class PowerOptions( @Group(HelpGroup.Launcher.toString) @HelpMessage("Allows to use restricted & experimental features") @Tag(tags.must) power: Boolean = false ) { def toCliArgs: List[String] = if power then List("--power") else Nil } object PowerOptions { implicit val parser: Parser[PowerOptions] = Parser.derive implicit val help: Help[PowerOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/launcher/ScalaRunnerLauncherOptions.scala ================================================ package scala.cli.launcher import caseapp.* import scala.cli.commands.shared.HelpGroup import scala.cli.commands.{Constants, tags} case class ScalaRunnerLauncherOptions( @Group(HelpGroup.Launcher.toString) @HelpMessage( s"The default version of Scala used when processing user inputs (current default: ${Constants.defaultScalaVersion}). Can be overridden with --scala-version. " ) @ValueDescription("version") @Hidden @Tag(tags.implementation) @Name("cliDefaultScalaVersion") cliUserScalaVersion: Option[String] = None, @Group(HelpGroup.Launcher.toString) @HelpMessage("") @Hidden @Tag(tags.implementation) @Name("r") @Name("repo") @Name("repository") @Name("predefinedRepository") cliPredefinedRepository: List[String] = Nil, @Group(HelpGroup.Launcher.toString) @HelpMessage( "This allows to override the program name identified by Scala CLI as itself (the default is 'scala-cli')" ) @Hidden @Tag(tags.implementation) progName: Option[String] = None, @Group(HelpGroup.Launcher.toString) @HelpMessage( "This allows to skip checking for newest Scala CLI versions. --offline covers this scenario as well." ) @Hidden @Tag(tags.implementation) skipCliUpdates: Option[Boolean] = None, @Hidden @Tag(tags.implementation) predefinedCliVersion: Option[String] = None, @Hidden @Tag(tags.implementation) @Name("initialLauncher") initialLauncherPath: Option[String] = None ) { def toCliArgs: List[String] = cliUserScalaVersion.toList.flatMap(v => List("--cli-default-scala-version", v)) ++ cliPredefinedRepository.flatMap(v => List("--repository", v)) ++ progName.toList.flatMap(v => List("--prog-name", v)) ++ skipCliUpdates.toList.filter(v => v).map(_ => "--skip-cli-updates") ++ predefinedCliVersion.toList.flatMap(v => List("--predefined-cli-version", v)) ++ initialLauncherPath.toList.flatMap(v => List("--initial-launcher-path", v)) } object ScalaRunnerLauncherOptions { implicit val parser: Parser[ScalaRunnerLauncherOptions] = Parser.derive implicit val help: Help[ScalaRunnerLauncherOptions] = Help.derive } ================================================ FILE: modules/cli/src/main/scala/scala/cli/packaging/Library.scala ================================================ package scala.cli.packaging import java.io.OutputStream import java.nio.file.StandardOpenOption.{CREATE, TRUNCATE_EXISTING} import java.nio.file.attribute.FileTime import java.util.jar.{Attributes as JarAttributes, JarOutputStream} import java.util.zip.{ZipEntry, ZipOutputStream} import scala.build.Build import scala.cli.internal.CachedBinary object Library { def libraryJar( builds: Seq[Build.Successful], mainClassOpt: Option[String] = None ): os.Path = { val workDir = builds.head.inputs.libraryJarWorkDir val dest = workDir / "library.jar" val cacheData = CachedBinary.getCacheData( builds, mainClassOpt.toList.flatMap(c => List("--main-class", c)), dest, workDir ) if cacheData.changed then { var outputStream: OutputStream = null try { outputStream = os.write.outputStream( dest, createFolders = true, openOptions = Seq(CREATE, TRUNCATE_EXISTING) ) writeLibraryJarTo( outputStream, builds, mainClassOpt ) } finally if outputStream != null then outputStream.close() CachedBinary.updateProjectAndOutputSha(dest, workDir, cacheData.projectSha) } dest } def writeLibraryJarTo( outputStream: OutputStream, builds: Seq[Build.Successful], mainClassOpt: Option[String] = None, hasActualManifest: Boolean = true, contentDirOverride: Option[os.Path] = None ): Unit = { val manifest = new java.util.jar.Manifest manifest.getMainAttributes.put(JarAttributes.Name.MANIFEST_VERSION, "1.0") if hasActualManifest then for { mainClass <- mainClassOpt.orElse(builds.flatMap(_.sources.defaultMainClass).headOption) if mainClass.nonEmpty } manifest.getMainAttributes.put(JarAttributes.Name.MAIN_CLASS, mainClass) var zos: ZipOutputStream = null val contentDirs = builds.map(b => contentDirOverride.getOrElse(b.output)).distinct try { zos = new JarOutputStream(outputStream, manifest) for { contentDir <- contentDirs path <- os.walk(contentDir) if os.isFile(path) } { val name = path.relativeTo(contentDir).toString val lastModified = os.mtime(path) val ent = new ZipEntry(name) ent.setLastModifiedTime(FileTime.fromMillis(lastModified)) val content = os.read.bytes(path) ent.setSize(content.length) zos.putNextEntry(ent) zos.write(content) zos.closeEntry() } } finally if (zos != null) zos.close() } extension (build: Build.Successful) { private def fullClassPathAsJar: Seq[os.Path] = Seq(libraryJar(Seq(build))) ++ build.dependencyClassPath def fullClassPathMaybeAsJar(asJar: Boolean): Seq[os.Path] = if asJar then fullClassPathAsJar else build.fullClassPath } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/packaging/NativeImage.scala ================================================ package scala.cli.packaging import java.io.File import scala.build.internal.{ManifestJar, Runner} import scala.build.internals.ConsoleUtils.ScalaCliConsole.warnPrefix import scala.build.internals.MsvcEnvironment import scala.build.internals.MsvcEnvironment.* import scala.build.{Build, Logger, Positioned, coursierVersion} import scala.cli.errors.GraalVMNativeImageError import scala.cli.graal.{BytecodeProcessor, TempCache} import scala.cli.internal.CachedBinary import scala.util.Properties object NativeImage { private def ensureHasNativeImageCommand( graalVMHome: os.Path, logger: Logger ): os.Path = { val ext = if (Properties.isWin) ".cmd" else "" val nativeImage = graalVMHome / "bin" / s"native-image$ext" if (os.exists(nativeImage)) logger.debug(s"$nativeImage found") else { val proc = os.proc(graalVMHome / "bin" / s"gu$ext", "install", "native-image") logger.debug(s"$nativeImage not found, running ${proc.command.flatMap(_.value)}") proc.call(stdin = os.Inherit, stdout = os.Inherit) if (!os.exists(nativeImage)) logger.message( s"Seems gu install command didn't install $nativeImage, trying to run it anyway" ) } nativeImage } /** Alias currentHome to the root of a drive, so that its files can be accessed with shorter paths * (hopefully not going above the ~260 char limit of some Windows apps, such as cl.exe). * * Couldn't manage to make * https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=powershell#enable-long-paths-in-windows-10-version-1607-and-later * work, so I went down the path here. */ private def maybeWithShorterGraalvmHome[T]( currentHome: os.Path, logger: Logger )( f: os.Path => T ): T = // Lower threshold (was 180) to ensure native-image's internal paths don't exceed 260-char limit if (Properties.isWin && currentHome.toString.length >= 90) { val (driveLetter, newHome) = getShortenedPath(currentHome, logger) val savedCodepage: String = getCodePage(logger) val result = try f(newHome) finally { unaliasDriveLetter(driveLetter) setCodePage(savedCodepage) } result } else f(currentHome) def buildNativeImage( builds: Seq[Build.Successful], mainClass: String, dest: os.Path, nativeImageWorkDir: os.Path, extraOptions: Seq[String], logger: Logger ): Unit = { os.makeDir.all(nativeImageWorkDir) val jvmId = builds.head.options.notForBloopOptions.packageOptions.nativeImageOptions.jvmId val options = builds.head.options.copy( javaOptions = builds.head.options.javaOptions.copy( jvmIdOpt = Some(Positioned.none(jvmId)) ) ) val javaHome = options.javaHome().value val nativeImageArgs = options.notForBloopOptions.packageOptions.nativeImageOptions.graalvmArgs.map(_.value) val cacheData = CachedBinary.getCacheData( builds, s"--java-home=${javaHome.javaHome.toString}" :: "--" :: extraOptions.toList ++ nativeImageArgs, dest, nativeImageWorkDir ) if cacheData.changed then { val mainJar = Library.libraryJar(builds) val originalClassPath = mainJar +: builds.flatMap(_.dependencyClassPath).distinct ManifestJar.maybeWithManifestClassPath( createManifest = Properties.isWin, classPath = originalClassPath, // seems native-image doesn't correctly parse paths in manifests - this is especially a problem on Windows wrongSimplePathsInManifest = true ) { processedClassPath => val needsProcessing = builds.head.scalaParams.map(_.scalaVersion.coursierVersion) .exists(sv => (sv >= "3.0.0".coursierVersion) && (sv < "3.3.0".coursierVersion)) if needsProcessing then logger.message( s"""$warnPrefix building native images with Scala 3 older than 3.3.0 is deprecated. |$warnPrefix support will be dropped in a future Scala CLI version. |$warnPrefix it is advised to upgrade to a more recent Scala version.""".stripMargin ) val (classPath, toClean, scala3extraOptions) = if needsProcessing then { val cpString = processedClassPath.mkString(File.pathSeparator) val processed = BytecodeProcessor.processClassPath(cpString, TempCache).toSeq val nativeConfigFile = os.temp(suffix = ".json") os.write.over( nativeConfigFile, """[ | { | "name": "sun.misc.Unsafe", | "allDeclaredConstructors": true, | "allPublicConstructors": true, | "allDeclaredMethods": true, | "allDeclaredFields": true | } |] |""".stripMargin ) val cp = processed.map(_.path) val options = Seq(s"-H:ReflectionConfigurationFiles=$nativeConfigFile") (cp, nativeConfigFile +: BytecodeProcessor.toClean(processed), options) } else (processedClassPath, Seq[os.Path](), Seq[String]()) def stripSuffixIgnoreCase(s: String, suffix: String): String = if (s.toLowerCase.endsWith(suffix.toLowerCase)) s.substring(0, s.length - suffix.length) else s try { val args = extraOptions ++ scala3extraOptions ++ Seq( s"-H:Path=${dest / os.up}", s"-H:Name=${stripSuffixIgnoreCase(dest.last, ".exe")}", // Case-insensitive strip suffix "-cp", classPath.map(_.toString).mkString(File.pathSeparator), mainClass ) ++ nativeImageArgs maybeWithShorterGraalvmHome(javaHome.javaHome, logger) { graalVMHome => val nativeImageCommand = ensureHasNativeImageCommand(graalVMHome, logger) val command = nativeImageCommand.toString +: args val exitCode = if Properties.isWin then MsvcEnvironment.msvcNativeImageProcess( command = command, workingDir = nativeImageWorkDir, logger = logger ) else Runner.run(command, logger).waitFor() if exitCode == 0 then { val actualDest = if Properties.isWin then if dest.last.endsWith(".exe") then dest else dest / os.up / s"${dest.last}.exe" else dest CachedBinary.updateProjectAndOutputSha( actualDest, nativeImageWorkDir, cacheData.projectSha ) } else throw new GraalVMNativeImageError } } finally util.Try(toClean.foreach(os.remove.all)) } } else logger.message("Found cached native image binary.") } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/publish/BouncycastleExternalSigner.scala ================================================ package scala.cli.publish import coursier.publish.Content import coursier.publish.signing.Signer import scala.build.Logger import scala.cli.signing.shared.PasswordOption import scala.util.Properties final case class BouncycastleExternalSigner( secretKey: PasswordOption, passwordOpt: Option[PasswordOption], command: Seq[String], logger: Logger ) extends Signer { private def withFileContent[T](content: Content)(f: os.Path => T): T = content match { case file: Content.File => f(os.Path(file.path, os.pwd)) case m: Content.InMemory => val permsOpt = if (Properties.isWin) None else Some("rw-------": os.PermSet) val tmpFile = os.temp(m.content0, perms = permsOpt.orNull) try f(tmpFile) finally os.remove(tmpFile) } def sign(content: Content): Either[String, String] = withFileContent(content) { path => val passwordArgs = passwordOpt.toSeq.flatMap(p => Seq("--password", p.asString.value)) val proc = os.proc( command, "pgp", "sign", passwordArgs, "--secret-key", secretKey.asString.value, "--stdout", path ) logger.debug(s"Running command ${proc.command.flatMap(_.value)}") val res = proc.call(stdin = os.Inherit, check = false) val output = res.out.trim() if (res.exitCode == 0) Right(output) else Left(output) } } object BouncycastleExternalSigner { def apply( secretKey: PasswordOption, passwordOrNull: PasswordOption, command: Array[String], logger: Logger ): BouncycastleExternalSigner = BouncycastleExternalSigner( secretKey, Option(passwordOrNull), command.toSeq, logger ) } ================================================ FILE: modules/cli/src/main/scala/scala/cli/publish/BouncycastleSignerMaker.scala ================================================ package scala.cli.publish import coursier.publish.signing.Signer import org.bouncycastle.jce.provider.BouncyCastleProvider import java.security.Security import java.util.function.Supplier import scala.build.Logger import scala.cli.signing.shared.PasswordOption import scala.cli.signing.util.BouncycastleSigner /** Used for choosing the right BouncyCastleSigner when Scala CLI is run on JVM.
* * See [[scala.cli.internal.BouncycastleSignerMakerSubst BouncycastleSignerMakerSubst]] */ class BouncycastleSignerMaker { def get( forceSigningExternally: java.lang.Boolean, passwordOrNull: PasswordOption, secretKey: PasswordOption, command: Supplier[Array[String]], // unused here, but used in the GraalVM substitution logger: Logger // unused here, but used in the GraalVM substitution ): Signer = if (forceSigningExternally) BouncycastleExternalSigner(secretKey, passwordOrNull, command.get, logger) else BouncycastleSigner(secretKey.getBytes(), Option(passwordOrNull).map(_.get())) def maybeInit(): Unit = Security.addProvider(new BouncyCastleProvider) } ================================================ FILE: modules/cli/src/main/scala/scala/cli/util/ArgHelpers.scala ================================================ package scala.cli.util import caseapp.core.Arg import caseapp.core.help.HelpFormat import caseapp.core.util.CaseUtil import scala.build.input.ScalaCliInvokeData import scala.build.internal.util.WarningMessages import scala.cli.ScalaCli import scala.cli.commands.shared.{HelpCommandGroup, HelpGroup} import scala.cli.commands.{SpecificationLevel, tags} object ArgHelpers { extension (arg: Arg) { private def hasTag(tag: String): Boolean = arg.tags.exists(_.name == tag) private def hasTagPrefix(tagPrefix: String): Boolean = arg.tags.exists(_.name.startsWith(tagPrefix)) def isExperimental: Boolean = arg.hasTag(tags.experimental) def isRestricted: Boolean = arg.hasTag(tags.restricted) def isDeprecated: Boolean = arg.hasTagPrefix(tags.deprecatedPrefix) def deprecatedNames: List[String] = arg.tags .filter(_.name.startsWith(tags.deprecatedPrefix)) .map(_.name.stripPrefix(s"${tags.deprecatedPrefix}${tags.valueSeparator}")) .toList def deprecatedOptionAliases: List[String] = arg.deprecatedNames.map { case name if name.startsWith("-") => name case name if name.length == 1 => "-" + name case name => "--" + CaseUtil.pascalCaseSplit(name.toCharArray.toList).map( _.toLowerCase ).mkString("-") } def isExperimentalOrRestricted: Boolean = arg.isRestricted || arg.isExperimental def isSupported: Boolean = ScalaCli.allowRestrictedFeatures || !arg.isExperimentalOrRestricted def isImportant: Boolean = arg.hasTag(tags.inShortHelp) def isMust: Boolean = arg.hasTag(tags.must) def level: SpecificationLevel = arg.tags .flatMap(t => tags.levelFor(t.name)) .headOption .getOrElse(SpecificationLevel.IMPLEMENTATION) def powerOptionUsedInSip(optionName: String)(using ScalaCliInvokeData): String = { val specificationLevel = if arg.isExperimental then SpecificationLevel.EXPERIMENTAL else if arg.isRestricted then SpecificationLevel.RESTRICTED else arg.tags .flatMap(t => tags.levelFor(t.name)) .headOption .getOrElse(SpecificationLevel.EXPERIMENTAL) WarningMessages.powerOptionUsedInSip(optionName, specificationLevel) } } extension (helpFormat: HelpFormat) { def withPrimaryGroup(primaryGroup: HelpGroup): HelpFormat = helpFormat.withPrimaryGroups(Seq(primaryGroup)) def withPrimaryGroups(primaryGroups: Seq[HelpGroup]): HelpFormat = { val primaryStringGroups = primaryGroups.map(_.toString) val oldSortedGroups = helpFormat.sortedGroups.getOrElse(Seq.empty) val filteredOldSortedGroups = oldSortedGroups.filterNot(primaryStringGroups.contains) helpFormat.copy(sortedGroups = Some(primaryStringGroups ++ filteredOldSortedGroups)) } def withHiddenGroups(hiddenGroups: Seq[HelpGroup]): HelpFormat = helpFormat.copy(hiddenGroups = Some(hiddenGroups.map(_.toString))) def withHiddenGroup(hiddenGroup: HelpGroup): HelpFormat = helpFormat.withHiddenGroups(Seq(hiddenGroup)) def withHiddenGroupsWhenShowHidden(hiddenGroups: Seq[HelpGroup]): HelpFormat = helpFormat.copy(hiddenGroupsWhenShowHidden = Some(hiddenGroups.map(_.toString))) def withHiddenGroupWhenShowHidden(hiddenGroup: HelpGroup): HelpFormat = helpFormat.withHiddenGroupsWhenShowHidden(Seq(hiddenGroup)) def withSortedGroups(sortedGroups: Seq[HelpGroup]): HelpFormat = helpFormat.copy(sortedGroups = Some(sortedGroups.map(_.toString))) def withSortedCommandGroups(sortedGroups: Seq[HelpCommandGroup]): HelpFormat = helpFormat.copy(sortedCommandGroups = Some(sortedGroups.map(_.toString))) def withNamesLimit(namesLimit: Int): HelpFormat = helpFormat.copy(namesLimit = Some(namesLimit)) } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/util/ArgParsers.scala ================================================ package scala.cli.util import caseapp.core.argparser.{ArgParser, SimpleArgParser} abstract class LowPriorityArgParsers { /** case-app [[ArgParser]] for [[MaybeConfigPasswordOption]] * * Given a lower priority than the one for `Option[MaybeConfigPasswordOption]`, as the latter * falls back to `None` when given an empty string (like in `--password ""`), while letting it be * automatically derived from this one (with the former parser and the generic [[ArgParser]] for * `Option[T]` from case-app) would fail on such empty input. */ implicit lazy val maybeConfigPasswordOptionArgParser: ArgParser[MaybeConfigPasswordOption] = SimpleArgParser.from("password") { str => MaybeConfigPasswordOption.parse(str) .left.map(caseapp.core.Error.Other(_)) } } object ArgParsers extends LowPriorityArgParsers { /** case-app [[ArgParser]] for `Option[MaybeConfigPasswordOption]` * * Unlike a parser automatically derived through case-app [[ArgParser]] for `Option[T]`, the * parser here accepts empty input (like in `--password ""`), and returns a `None` value in that * case. */ implicit lazy val optionMaybeConfigPasswordOptionArgParser : ArgParser[Option[MaybeConfigPasswordOption]] = SimpleArgParser.from("password") { str => if (str.trim.isEmpty) Right(None) else maybeConfigPasswordOptionArgParser(None, -1, -1, str).map(Some(_)) } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/util/ConfigDbUtils.scala ================================================ package scala.cli.util import scala.build.errors.{BuildException, ConfigDbException} import scala.build.{Directories, Logger} import scala.cli.commands.publish.ConfigUtil.wrapConfigException import scala.cli.config.{ConfigDb, Key} object ConfigDbUtils { private def getLatestConfigDb: Either[ConfigDbException, ConfigDb] = ConfigDb.open(Directories.directories.dbPath.toNIO).wrapConfigException lazy val configDb: Either[ConfigDbException, ConfigDb] = getLatestConfigDb extension [T](either: Either[Exception, T]) { private def handleConfigDbException(f: BuildException => Unit): Option[T] = either match case Left(e: BuildException) => f(e) None case Left(e: Exception) => f(new ConfigDbException(e)) None case Right(value) => Some(value) } def getConfigDbOpt(logger: Logger): Option[ConfigDb] = configDb.handleConfigDbException(logger.debug) def getLatestConfigDbOpt(logger: Logger): Option[ConfigDb] = getLatestConfigDb.handleConfigDbException(logger.debug) extension (db: ConfigDb) { def getOpt[T](configDbKey: Key[T], f: BuildException => Unit): Option[T] = db.get(configDbKey).handleConfigDbException(f).flatten def getOpt[T](configDbKey: Key[T], logger: Logger): Option[T] = getOpt(configDbKey, logger.debug(_)) def getOpt[T](configDbKey: Key[T]): Option[T] = getOpt(configDbKey, _.printStackTrace(System.err)) } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/util/ConfigPasswordOptionHelpers.scala ================================================ package scala.cli.util import scala.build.errors.BuildException import scala.build.options.publish.ConfigPasswordOption import scala.cli.commands.SpecificationLevel import scala.cli.commands.publish.ConfigUtil.* import scala.cli.config.{ConfigDb, Key, PasswordOption} import scala.cli.errors.MissingConfigEntryError object ConfigPasswordOptionHelpers { implicit class ConfigPasswordOptionOps(private val opt: ConfigPasswordOption) extends AnyVal { def get(configDb: => ConfigDb): Either[BuildException, PasswordOption] = opt match { case a: ConfigPasswordOption.ActualOption => Right(a.option.toConfig) case c: ConfigPasswordOption.ConfigOption => val key = new Key.PasswordEntry(c.prefix, c.name, SpecificationLevel.IMPLEMENTATION) configDb.get(key).wrapConfigException.flatMap { case None => Left(new MissingConfigEntryError(c.fullName)) case Some(value) => Right(value) } } } } ================================================ FILE: modules/cli/src/main/scala/scala/cli/util/MaybeConfigPasswordOption.scala ================================================ package scala.cli.util import scala.build.options.publish.ConfigPasswordOption import scala.cli.signing.shared.PasswordOption /** Can be either a [[PasswordOption]], or something like "config:…" pointing at a config entry */ sealed abstract class MaybeConfigPasswordOption extends Product with Serializable { def configPasswordOptions() = this match { case MaybeConfigPasswordOption.ActualOption(option) => ConfigPasswordOption.ActualOption(option) case MaybeConfigPasswordOption.ConfigOption(fullName) => ConfigPasswordOption.ConfigOption(fullName) } } object MaybeConfigPasswordOption { final case class ActualOption(option: PasswordOption) extends MaybeConfigPasswordOption final case class ConfigOption(fullName: String) extends MaybeConfigPasswordOption def parse(input: String): Either[String, MaybeConfigPasswordOption] = if (input.startsWith("config:")) Right(ConfigOption(input.stripPrefix("config:"))) else PasswordOption.parse(input).map(ActualOption(_)) } ================================================ FILE: modules/cli/src/main/scala/scala/cli/util/SeqHelpers.scala ================================================ package scala.cli.util object SeqHelpers { implicit class StringSeqOpt(val seq: Seq[String]) extends AnyVal { def appendOnInit(s: String): Seq[String] = if seq.isEmpty || seq.tail.isEmpty then seq else (seq.head + s) +: seq.tail.appendOnInit(s) } } ================================================ FILE: modules/cli/src/test/scala/cli/commands/tests/DocTests.scala ================================================ package scala.cli.commands.tests import com.eed3si9n.expecty.Expecty.assert as expect import scala.build.CrossBuildParams import scala.build.internal.Constants import scala.cli.commands.doc.Doc class DocTests extends munit.FunSuite { test("crossDocSubdirName: single cross group yields empty subdir") { val params = CrossBuildParams(Constants.defaultScala213Version, "jvm") expect(Doc.crossDocSubdirName( params, multipleCrossGroups = false, needsPlatformInSuffix = false ) == "") expect(Doc.crossDocSubdirName( params, multipleCrossGroups = false, needsPlatformInSuffix = true ) == "") } test("crossDocSubdirName: multiple groups, single platform uses only Scala version") { val params = CrossBuildParams(Constants.scala3Lts, "jvm") expect( Doc.crossDocSubdirName(params, multipleCrossGroups = true, needsPlatformInSuffix = false) == Constants.scala3Lts ) } test("crossDocSubdirName: multiple groups and platforms include platform in suffix") { val paramsJvm = CrossBuildParams(Constants.defaultScala213Version, "jvm") val paramsJs = CrossBuildParams(Constants.defaultScala213Version, "js") val paramsNat = CrossBuildParams(Constants.scala3Lts, "native") expect( Doc.crossDocSubdirName(paramsJvm, multipleCrossGroups = true, needsPlatformInSuffix = true) == s"${Constants.defaultScala213Version}_jvm" ) expect( Doc.crossDocSubdirName(paramsJs, multipleCrossGroups = true, needsPlatformInSuffix = true) == s"${Constants.defaultScala213Version}_js" ) expect( Doc.crossDocSubdirName(paramsNat, multipleCrossGroups = true, needsPlatformInSuffix = true) == s"${Constants.scala3Lts}_native" ) } for (javaVersion <- Constants.mainJavaVersions) test(s"correct external mappings for JVM $javaVersion") { val args = Doc.defaultScaladocArgs(Constants.defaultScalaVersion, javaVersion) val mappingsArg = args.find(_.startsWith("-external-mappings:")).get if javaVersion >= 11 then expect(mappingsArg.contains(s"javase/$javaVersion/docs/api/java.base/")) else expect(mappingsArg.contains(s"javase/$javaVersion/docs/api/")) expect(!mappingsArg.contains("java.base/")) expect(mappingsArg.contains(s"scala-lang.org/api/${Constants.defaultScalaVersion}/")) } test(s"correct external mappings for Scala 3 LTS (${Constants.scala3Lts})") { val args = Doc.defaultScaladocArgs(Constants.scala3Lts, Constants.defaultJavaVersion) val mappingsArg = args.find(_.startsWith("-external-mappings:")).get expect(mappingsArg.contains(s"scala-lang.org/api/${Constants.scala3Lts}/")) expect( mappingsArg.contains(s"javase/${Constants.defaultJavaVersion}/docs/api/java.base/") ) } test(s"correct external mappings for default Scala (${Constants.defaultScalaVersion})") { val args = Doc.defaultScaladocArgs(Constants.defaultScalaVersion, Constants.defaultJavaVersion) val mappingsArg = args.find(_.startsWith("-external-mappings:")).get expect(mappingsArg.contains(s"scala-lang.org/api/${Constants.defaultScalaVersion}/")) expect( mappingsArg.contains(s"javase/${Constants.defaultJavaVersion}/docs/api/java.base/") ) } } ================================================ FILE: modules/cli/src/test/scala/cli/commands/tests/ReplOptionsTests.scala ================================================ package scala.cli.commands.tests import com.eed3si9n.expecty.Expecty.assert as expect import scala.build.internal.Constants import scala.cli.commands.repl.{Repl, ReplOptions, SharedReplOptions} import scala.cli.commands.shared.{SharedOptions, SharedPythonOptions} class ReplOptionsTests extends munit.FunSuite { test("ScalaPy version") { val ver = "X.Y.Z" val replOptions = ReplOptions( shared = SharedOptions( sharedPython = SharedPythonOptions( scalaPyVersion = Some(ver) ) ) ) val buildOptions = Repl.buildOptions(replOptions).value expect(buildOptions.notForBloopOptions.scalaPyVersion.contains(ver)) } test("Downgrade Scala version if needed") { val replOptions = ReplOptions( sharedRepl = SharedReplOptions( ammonite = Some(true) ) ) val maxVersion = "3.1.3" val maxLtsVersion = Constants.scala3Lts val buildOptions = Repl.buildOptions0(replOptions, maxVersion, maxLtsVersion) expect(buildOptions.scalaOptions.scalaVersion.flatMap(_.versionOpt).contains(maxVersion)) } } ================================================ FILE: modules/cli/src/test/scala/cli/commands/tests/RunOptionsTests.scala ================================================ package scala.cli.commands.tests import com.eed3si9n.expecty.Expecty.assert as expect import scala.cli.commands.run.{Run, RunOptions} import scala.cli.commands.shared.{SharedOptions, SharedPythonOptions} class RunOptionsTests extends munit.FunSuite { test("ScalaPy version") { val ver = "X.Y.Z" val runOptions = RunOptions( shared = SharedOptions( sharedPython = SharedPythonOptions( scalaPyVersion = Some(ver) ) ) ) val buildOptions = Run.buildOptions(runOptions).value expect(buildOptions.notForBloopOptions.scalaPyVersion.contains(ver)) } test("resolve toolkit dependency") { val runOptions = RunOptions( shared = SharedOptions( withToolkit = Some("latest") ) ) val buildOptions = Run.buildOptions(runOptions).value val dep = buildOptions.classPathOptions.extraDependencies.toSeq.headOption assert(dep.nonEmpty) val toolkitDep = dep.get.value expect(toolkitDep.organization == "org.scala-lang") expect(toolkitDep.name == "toolkit") expect(toolkitDep.version == "latest.release") } test("resolve typelevel toolkit dependency") { val runOptions = RunOptions( shared = SharedOptions( withToolkit = Some("typelevel:latest") ) ) val buildOptions = Run.buildOptions(runOptions).value val dep = buildOptions.classPathOptions.extraDependencies.toSeq.headOption assert(dep.nonEmpty) val toolkitDep = dep.get.value expect(toolkitDep.organization == "org.typelevel") expect(toolkitDep.name == "toolkit") expect(toolkitDep.version == "latest.release") } } ================================================ FILE: modules/cli/src/test/scala/cli/tests/ArgSplitterTest.scala ================================================ package cli.tests import scala.cli.commands.shared.ArgSplitter class ArgSplitterTest extends TestUtil.ScalaCliSuite { test("test scalac options are split correctly") { val args = List( List("-arg", "-other-arg"), List("-yet-another-arg", "dir/path\\ with\\ space", "'another arg with space'"), List("\"yet another arg with space\"") ) val input = args.map(_.mkString(" ", " ", "")).mkString(" ", "\n", "") assertEquals(ArgSplitter.splitToArgs(input), args.flatten) } } ================================================ FILE: modules/cli/src/test/scala/cli/tests/CachedBinaryTests.scala ================================================ package scala.cli.tests import bloop.rifle.BloopRifleConfig import cli.tests.TestUtil import com.eed3si9n.expecty.Expecty.assert as expect import os.Path import scala.build.options.{BuildOptions, InternalOptions} import scala.build.tests.util.BloopServer import scala.build.tests.{TestInputs, TestLogger} import scala.build.{Build, BuildThreads, Directories, LocalRepo} import scala.cli.internal.CachedBinary import scala.util.{Properties, Random} class CachedBinaryTests extends TestUtil.ScalaCliSuite { val buildThreads: BuildThreads = BuildThreads.create() def bloopConfig: BloopRifleConfig = BloopServer.bloopConfig val helloFileName = "Hello.scala" val inputs: TestInputs = TestInputs( os.rel / helloFileName -> s"""object Hello extends App { | println("Hello") |} |""".stripMargin, os.rel / "main" / "Main.scala" -> s"""object Main extends App { | println("Hello") |} |""".stripMargin ) val extraRepoTmpDir: Path = os.temp.dir(prefix = "scala-cli-tests-extra-repo-") val directories: Directories = Directories.under(extraRepoTmpDir) val defaultOptions: BuildOptions = BuildOptions( internal = InternalOptions( localRepository = LocalRepo.localRepo(directories.localRepoDir, TestLogger()) ) ) for { fromDirectory <- List(false, true) additionalMessage = if (fromDirectory) "built from a directory" else "built from a set of files" } { test(s"should build native app with added test scope at first time ($additionalMessage)") { TestInputs( os.rel / "main" / "Main.scala" -> s"""object Main extends App { | println("Hello") |} |""".stripMargin, os.rel / "test" / "TestScope.scala" -> s"""object TestScope extends App { | println("Hello from the test scope") |} |""".stripMargin ).withLoadedBuilds( defaultOptions, buildThreads, Some(bloopConfig), fromDirectory ) { (_, _, builds) => expect(builds.builds.forall(_.success)) val config = builds.main.options.scalaNativeOptions.configCliOptions(resourcesExist = false) val nativeWorkDir = builds.main.inputs.nativeWorkDir val destPath = nativeWorkDir / s"main${if (Properties.isWin) ".exe" else ""}" // generate dummy output os.write(destPath, Random.alphanumeric.take(10).mkString(""), createFolders = true) val successfulBuilds = builds.builds.map { case s: Build.Successful => s } val cacheData = CachedBinary.getCacheData(successfulBuilds, config, destPath, nativeWorkDir) expect(cacheData.changed) } } test(s"should build native app at first time ($additionalMessage)") { inputs.withLoadedBuild(defaultOptions, buildThreads, Some(bloopConfig), fromDirectory) { (_, _, maybeBuild) => val build = maybeBuild.successfulOpt.get val config = build.options.scalaNativeOptions.configCliOptions(resourcesExist = false) val nativeWorkDir = build.inputs.nativeWorkDir val destPath = nativeWorkDir / s"main${if (Properties.isWin) ".exe" else ""}" // generate dummy output os.write(destPath, Random.alphanumeric.take(10).mkString(""), createFolders = true) val cacheData = CachedBinary.getCacheData(Seq(build), config, destPath, nativeWorkDir) expect(cacheData.changed) } } test(s"should not rebuild the second time ($additionalMessage)") { inputs.withLoadedBuild(defaultOptions, buildThreads, Some(bloopConfig), fromDirectory) { (_, _, maybeBuild) => val build = maybeBuild.successfulOpt.get val config = build.options.scalaNativeOptions.configCliOptions(resourcesExist = false) val nativeWorkDir = build.inputs.nativeWorkDir val destPath = nativeWorkDir / s"main${if (Properties.isWin) ".exe" else ""}" // generate dummy output os.write(destPath, Random.alphanumeric.take(10).mkString(""), createFolders = true) val cacheData = CachedBinary.getCacheData(Seq(build), config, destPath, nativeWorkDir) CachedBinary.updateProjectAndOutputSha( destPath, nativeWorkDir, cacheData.projectSha ) expect(cacheData.changed) val sameBuildCache = CachedBinary.getCacheData(Seq(build), config, destPath, nativeWorkDir) expect(!sameBuildCache.changed) } } test(s"should build native if output file was deleted ($additionalMessage)") { inputs.withLoadedBuild(defaultOptions, buildThreads, Some(bloopConfig), fromDirectory) { (_, _, maybeBuild) => val build = maybeBuild.successfulOpt.get val config = build.options.scalaNativeOptions.configCliOptions(resourcesExist = false) val nativeWorkDir = build.inputs.nativeWorkDir val destPath = nativeWorkDir / s"main${if (Properties.isWin) ".exe" else ""}" // generate dummy output os.write(destPath, Random.alphanumeric.take(10).mkString(""), createFolders = true) val cacheData = CachedBinary.getCacheData(Seq(build), config, destPath, nativeWorkDir) CachedBinary.updateProjectAndOutputSha( destPath, nativeWorkDir, cacheData.projectSha ) expect(cacheData.changed) os.remove(destPath) val afterDeleteCache = CachedBinary.getCacheData(Seq(build), config, destPath, nativeWorkDir) expect(afterDeleteCache.changed) } } test(s"should build native if output file was changed ($additionalMessage)") { inputs.withLoadedBuild(defaultOptions, buildThreads, Some(bloopConfig), fromDirectory) { (_, _, maybeBuild) => val build = maybeBuild.successfulOpt.get val config = build.options.scalaNativeOptions.configCliOptions(resourcesExist = false) val nativeWorkDir = build.inputs.nativeWorkDir val destPath = nativeWorkDir / s"main${if (Properties.isWin) ".exe" else ""}" // generate dummy output os.write(destPath, Random.alphanumeric.take(10).mkString(""), createFolders = true) val cacheData = CachedBinary.getCacheData(Seq(build), config, destPath, nativeWorkDir) CachedBinary.updateProjectAndOutputSha( destPath, nativeWorkDir, cacheData.projectSha ) expect(cacheData.changed) os.write.over(destPath, Random.alphanumeric.take(10).mkString("")) val cacheAfterFileUpdate = CachedBinary.getCacheData(Seq(build), config, destPath, nativeWorkDir) expect(cacheAfterFileUpdate.changed) } } test(s"should build native if input file was changed ($additionalMessage)") { inputs.withLoadedBuild(defaultOptions, buildThreads, Some(bloopConfig), fromDirectory) { (root, _, maybeBuild) => val build = maybeBuild.successfulOpt.get val config = build.options.scalaNativeOptions.configCliOptions(resourcesExist = false) val nativeWorkDir = build.inputs.nativeWorkDir val destPath = nativeWorkDir / s"main${if (Properties.isWin) ".exe" else ""}" os.write(destPath, Random.alphanumeric.take(10).mkString(""), createFolders = true) val cacheData = CachedBinary.getCacheData(Seq(build), config, destPath, nativeWorkDir) CachedBinary.updateProjectAndOutputSha( destPath, nativeWorkDir, cacheData.projectSha ) expect(cacheData.changed) os.write.append(root / helloFileName, Random.alphanumeric.take(10).mkString("")) val cacheAfterFileUpdate = CachedBinary.getCacheData(Seq(build), config, destPath, nativeWorkDir) expect(cacheAfterFileUpdate.changed) } } test(s"should build native if native config was changed ($additionalMessage)") { inputs.withLoadedBuild(defaultOptions, buildThreads, Some(bloopConfig), fromDirectory) { (_, _, maybeBuild) => val build = maybeBuild.successfulOpt.get val config = build.options.scalaNativeOptions.configCliOptions(resourcesExist = false) val nativeWorkDir = build.inputs.nativeWorkDir val destPath = nativeWorkDir / s"main${if (Properties.isWin) ".exe" else ""}" os.write(destPath, Random.alphanumeric.take(10).mkString(""), createFolders = true) val cacheData = CachedBinary.getCacheData(Seq(build), config, destPath, nativeWorkDir) CachedBinary.updateProjectAndOutputSha( destPath, nativeWorkDir, cacheData.projectSha ) expect(cacheData.changed) val updatedBuild = build.copy( options = build.options.copy( scalaNativeOptions = build.options.scalaNativeOptions.copy( clang = Some(Random.alphanumeric.take(10).mkString("")) ) ) ) val updatedConfig = updatedBuild.options.scalaNativeOptions.configCliOptions(resourcesExist = false) val cacheAfterConfigUpdate = CachedBinary.getCacheData( Seq(updatedBuild), updatedConfig, destPath, nativeWorkDir ) expect(cacheAfterConfigUpdate.changed) } } } } ================================================ FILE: modules/cli/src/test/scala/cli/tests/HelpCheck.scala ================================================ package cli.tests import com.eed3si9n.expecty.Expecty.expect import scala.cli.ScalaCliCommands import scala.cli.commands.version.Version class HelpCheck extends TestUtil.ScalaCliSuite { test("help message should be shorter then 80 lines") { val scalaCli = new ScalaCliCommands("scala-cli", "scala-cli", "Scala CLI") val helpMessage = scalaCli.help.help(scalaCli.helpFormat) val lines = helpMessage.split("\r\n|\r|\n").length assert(lines <= 80) } test( "version help message should only contain relevant options" ) { // regression test - https://github.com/VirtusLab/scala-cli/issues/1666 val helpMessage = Version.finalHelp.help(Version.helpFormat) expect(helpMessage.contains("Version options:")) expect(!helpMessage.contains("--usage")) expect(!helpMessage.contains("Logging options:")) expect(!helpMessage.contains("Other options:")) } } ================================================ FILE: modules/cli/src/test/scala/cli/tests/LauncherCliTest.scala ================================================ package cli.tests import com.eed3si9n.expecty.Expecty.expect import dependency.ScalaParameters import scala.build.internal.Constants import scala.build.tests.TestLogger import scala.cli.commands.shared.CoursierOptions import scala.cli.launcher.LauncherCli class LauncherCliTest extends TestUtil.ScalaCliSuite { test("resolve nightly version".flaky) { val logger = TestLogger() val cache = CoursierOptions().coursierCache(logger) val scalaParameters = ScalaParameters(Constants.defaultScalaVersion) val nightlyCliVersion = LauncherCli.resolveNightlyScalaCliVersion(cache, scalaParameters.scalaBinaryVersion) expect(nightlyCliVersion.endsWith("-SNAPSHOT")) } val expectedScalaCliVersions: Seq[(String, String)] = Seq( "0.1.2" -> Constants.defaultScala212Version, "0.1.1+43-g15666b67-SNAPSHOT" -> Constants.defaultScala212Version, "0.1.3" -> Constants.defaultScala213Version, "nightly" -> Constants.defaultScalaVersion ) for ((cliVersion, expectedScalaVersion) <- expectedScalaCliVersions) test(s"use expected scala version for Scala CLI launcher: $cliVersion") { val scalaVersion = LauncherCli.scalaCliScalaVersion(cliVersion) expect(scalaVersion == expectedScalaVersion) } } ================================================ FILE: modules/cli/src/test/scala/cli/tests/OptionsCheck.scala ================================================ package scala.cli.tests import cli.tests.TestUtil import scala.cli.ScalaCliCommands import scala.cli.commands.shared.HasGlobalOptions class OptionsCheck extends TestUtil.ScalaCliSuite { for (command <- new ScalaCliCommands("scala-cli", "scala-cli", "Scala CLI").commands) test(s"No duplicated options in ${command.names.head.mkString(" ")}") { command.ensureNoDuplicates() } test(s"--power option present in $command") { command.parser.stopAtFirstUnrecognized.parse(Seq("--power")) match { case Right((_: HasGlobalOptions, _ +: _)) => fail("Expected --power to be recognized") case _ => () } } } ================================================ FILE: modules/cli/src/test/scala/cli/tests/PackageTests.scala ================================================ package cli.tests import bloop.rifle.BloopRifleConfig import com.eed3si9n.expecty.Expecty.expect import java.nio.file.FileSystems import scala.build.Ops.* import scala.build.options.{BuildOptions, InternalOptions, PackageType} import scala.build.tests.util.BloopServer import scala.build.tests.{TestInputs, TestLogger} import scala.build.{BuildThreads, Directories, LocalRepo} import scala.cli.commands.package0.Package import scala.cli.packaging.Library class PackageTests extends TestUtil.ScalaCliSuite { val buildThreads: BuildThreads = BuildThreads.create() def bloopConfig: BloopRifleConfig = BloopServer.bloopConfig val extraRepoTmpDir: os.Path = os.temp.dir(prefix = "scala-cli-tests-extra-repo-") val directories: Directories = Directories.under(extraRepoTmpDir) val defaultOptions = BuildOptions( internal = InternalOptions( localRepository = LocalRepo.localRepo(directories.localRepoDir, TestLogger()) ) ) /** Fixes - https://github.com/VirtusLab/scala-cli/issues/2166 */ test(s"should generate a correct jar library when the project was changed") { TestInputs().fromRoot { root => val inputs = TestInputs( files = Seq(os.rel / "Hello.scala" -> """//> using platform scala-js |//> using options -Wvalue-discard -Wunused:all | |object Hello extends App { | println("Hello, World World World") |}""".stripMargin), forceCwd = Some(root) ) inputs.withBuild(defaultOptions, buildThreads, Some(bloopConfig)) { (_, _, maybeFirstBuild) => val firstBuild = maybeFirstBuild.orThrow.successfulOpt.get val firstLibraryJar = Library.libraryJar(Seq(firstBuild)) expect(os.exists(firstLibraryJar)) // should create library jar // change Hello.scala and recompile os.write.over( root / "Hello.scala", """//> using platform scala-js |//> using options -Wvalue-discard -Wunused:all | |object Hello extends App { | println("hello") |}""".stripMargin ) inputs.withBuild( defaultOptions, buildThreads, Some(bloopConfig), skipCreatingSources = true ) { (_, _, maybeSecondBuild) => val secondBuild = maybeSecondBuild.orThrow.successfulOpt.get val libraryJar = Library.libraryJar(Seq(secondBuild)) val fs = // should not throw "invalid CEN header (bad signature)" ZipException FileSystems.newFileSystem(libraryJar.toNIO, null: ClassLoader) expect(fs.isOpen) fs.close() } } } } /** Fixes - https://github.com/VirtusLab/scala-cli/issues/2303 */ test("accept packageType-native when using native platform") { val inputs = TestInputs( files = Seq(os.rel / "Hello.scala" -> """//> using platform native |//> using packaging.packageType native | |object Hello extends App { | println("Hello World") |}""".stripMargin) ) inputs.withBuild(defaultOptions, buildThreads, Some(bloopConfig)) { (_, _, maybeFirstBuild) => val build = maybeFirstBuild.orThrow.successfulOpt.get val packageType = Package.resolvePackageType(Seq(build), None).orThrow expect(packageType == PackageType.Native.Application) } } } ================================================ FILE: modules/cli/src/test/scala/cli/tests/ScalafmtTests.scala ================================================ package cli.tests import com.eed3si9n.expecty.Expecty.expect import com.github.plokhotnyuk.jsoniter_scala.core.* import com.github.plokhotnyuk.jsoniter_scala.macros.* import scala.build.Ops.* import scala.build.internal.Constants import scala.build.tests.{TestInputs, TestLogger} import scala.cli.commands.fmt.{FmtOptions, FmtUtil} class ScalafmtTests extends TestUtil.ScalaCliSuite { private lazy val defaultScalafmtVersion = Constants.defaultScalafmtVersion test("readVersionFromFile with non-default scalafmt version") { val confFile = """runner.dialect = scala213 |version = "3.1.2" |""".stripMargin TestInputs.withTmpDir("temp-dir") { dirPath => val confFilePath = dirPath / ".scalafmt.conf" os.write(confFilePath, confFile) val readVersionAndDialect = FmtUtil.readVersionAndDialect(workspace = dirPath, FmtOptions(), TestLogger()) expect(readVersionAndDialect == (Some("3.1.2"), Some("scala213"), Some(confFilePath))) } } test("readVersionFromFile with missing .scalafmt.conf file") { TestInputs.withTmpDir("temp-dir") { dirPath => val readVersionAndDialect = FmtUtil.readVersionAndDialect(workspace = dirPath, FmtOptions(), TestLogger()) expect(readVersionAndDialect == (None, None, None)) } } test(s"check native launcher availability for scalafmt $defaultScalafmtVersion") { final case class Asset(name: String) final case class Release(tag_name: String, assets: List[Asset]) lazy val releaseCodec: JsonValueCodec[Release] = JsonCodecMaker.make val url = s"https://api.github.com/repos/scalameta/scalafmt/releases/tags/v$defaultScalafmtVersion" val expectedAssets = Seq( "scalafmt-x86_64-apple-darwin.zip", "scalafmt-x86_64-pc-linux.zip", "scalafmt-x86_64-pc-win32.zip", "scalafmt-aarch64-apple-darwin.zip", "scalafmt-aarch64-pc-linux.zip" ) val errorMsg = s"""scalafmt native images missing for v$defaultScalafmtVersion at https://github.com/scalameta/scalafmt |Ensure that all expected assets are available in the release: | ${expectedAssets.mkString(", ")} |under tag v$defaultScalafmtVersion.""".stripMargin try { val resp = TestUtil.downloadFile(url).orThrow val release = readFromArray(resp)(using releaseCodec) val assets = release.assets.map(_.name) assert( expectedAssets.forall(assets.contains), clue = errorMsg ) } catch { case e: JsonReaderException => throw new Exception(s"Error reading $url", e) case e: Throwable => throw new Exception( s"""Failed to check for the ScalaFmt $defaultScalafmtVersion native launcher assets: ${e.getMessage} | |$errorMsg |""".stripMargin, e ) } } } ================================================ FILE: modules/cli/src/test/scala/cli/tests/SetupScalaCLITests.scala ================================================ package cli.tests import com.eed3si9n.expecty.Expecty.expect import java.util.{Collections, Properties} import scala.build.internal.Constants import scala.build.tests.TestInputs import scala.cli.ScalaCli import scala.jdk.CollectionConverters.{MapHasAsJava, MapHasAsScala} class SetupScalaCLITests extends TestUtil.ScalaCliSuite { test(s"should read java properties from file") { val key = "scala-cli" val value = "true" val inputs = TestInputs( os.rel / Constants.jvmPropertiesFileName -> s"""-Xignored_1 |-Xignored_2 |-Xignored_3 |-D$key=$value |""".stripMargin ) inputs.fromRoot(root => // save current props to restore them after test val currentProps = System.getProperties.clone().asInstanceOf[Properties] ScalaCli.loadJavaProperties(root) expect(sys.props.get(key).contains(value)) expect(sys.props.get("ignored_1").isEmpty) expect(sys.props.get("ignored_2").isEmpty) expect(sys.props.get("ignored_3").isEmpty) // restore original props System.setProperties(currentProps) ) } test(s"should read java properties from JAVA_OPTS and JDK_JAVA_OPTIONS") { // Adapted from https://stackoverflow.com/a/496849 def setEnvVars(newEnv: Map[String, String]): Unit = { val classes = classOf[Collections].getDeclaredClasses val env = System.getenv() for (cl <- classes) if (cl.getName.equals("java.util.Collections$UnmodifiableMap")) { val field = cl.getDeclaredField("m") field.setAccessible(true) val obj = field.get(env) val map = obj.asInstanceOf[java.util.Map[String, String]] map.clear() map.putAll(newEnv.asJava) } } val javaOptsValues = " -Xignored_1 -Dhttp.proxy=4.4.4.4 -Xignored_2" val jdkJavaOptionsValues = " -Xignored_3 -Dscala-cli=true -Xignored_4" TestInputs().fromRoot(root => // val currentEnv = System.getenv().asScala.toMap // modify environment variable of this process setEnvVars(Map("JAVA_OPTS" -> javaOptsValues, "JDK_JAVA_OPTIONS" -> jdkJavaOptionsValues)) ScalaCli.loadJavaProperties(root) expect(sys.props.get("http.proxy").contains("4.4.4.4")) expect(sys.props.get("scala-cli").contains("true")) expect(sys.props.get("ignored_1").isEmpty) expect(sys.props.get("ignored_2").isEmpty) expect(sys.props.get("ignored_3").isEmpty) expect(sys.props.get("ignored_4").isEmpty) // reset the env setEnvVars(currentEnv) ) } } ================================================ FILE: modules/cli/src/test/scala/cli/tests/TestUtil.scala ================================================ package cli.tests import coursier.cache.{ArtifactError, FileCache} import coursier.util.{Artifact, Task} import munit.AnyFixture import java.io.File import java.util.concurrent.TimeUnit import scala.concurrent.duration.{DurationInt, FiniteDuration} import scala.util.control.NonFatal object TestUtil { abstract class ScalaCliSuite extends munit.FunSuite { extension (munitContext: BeforeEach | AfterEach) { def locationAbsolutePath: os.Path = os.Path { (munitContext match { case beforeEach: BeforeEach => beforeEach.test case afterEach: AfterEach => afterEach.test }).location.path } } override def munitTimeout = new FiniteDuration(120, TimeUnit.SECONDS) val testStartEndLogger: Fixture[Unit] = new Fixture[Unit]("files") { def apply(): Unit = () override def beforeEach(context: BeforeEach): Unit = { val fileName = context.locationAbsolutePath.baseName System.err.println( s">==== ${Console.CYAN}Running '${context.test.name}' from $fileName${Console.RESET}" ) } override def afterEach(context: AfterEach): Unit = { val fileName = context.locationAbsolutePath.baseName System.err.println( s"X==== ${Console.CYAN}Finishing '${context.test.name}' from $fileName${Console.RESET}" ) } } override def munitFixtures: Seq[AnyFixture[?]] = List(testStartEndLogger) } def downloadFile(url: String): Either[ArtifactError, Array[Byte]] = { val artifact = Artifact(url).withChanging(true) val cache = FileCache() val file: Either[ArtifactError, File] = cache.logger.use { try cache.withTtl(0.seconds).file(artifact).run.unsafeRun()(using cache.ec) catch { case NonFatal(e) => throw new Exception(e) } } file.map(f => os.read.bytes(os.Path(f, os.pwd))) } val isCI: Boolean = System.getenv("CI") != null } ================================================ FILE: modules/cli/src/test/scala/scala/cli/commands/publish/IvyTests.scala ================================================ package scala.cli.commands.publish import coursier.core.{ModuleName, Organization, Type} import coursier.publish.Pom import java.time.LocalDateTime class IvyTests extends munit.FunSuite { test("ivy includes Apache Ivy license and Maven POM scm and developers") { val organization = Organization("org.example") val moduleName = ModuleName("demo") val version = "1.0" val description = "A demo" val homepage = "https://example.org" val pomProjectName = "Demo library" val packaging = Type("jar") val licenseName = "Apache-2.0" val licenseUrl = "https://spdx.org/licenses/Apache-2.0.html" val scmUrl = "https://github.com/foo/bar.git" val scmConnection = "scm:git:github.com/foo/bar.git" val scmDevConnection = "scm:git:git@github.com:foo/bar.git" val devId = "jdu" val devName = "Jane" val devUrl = "https://jane.example" val devMail = "jane@example.org" val fixedTime = LocalDateTime.of(2024, 1, 2, 3, 4, 5) val xml = Ivy.create( organization = organization, moduleName = moduleName, version = version, description = Some(description), url = Some(homepage), pomProjectName = Some(pomProjectName), packaging = Some(packaging), license = Some(Pom.License(licenseName, licenseUrl)), scm = Some(Pom.Scm(scmUrl, scmConnection, scmDevConnection)), developers = Seq( Pom.Developer(devId, devName, devUrl, Some(devMail)) ), dependencies = Nil, time = fixedTime ) assert(xml.contains(s"""$pomProjectName")) assert(xml.contains(s"${packaging.value}")) assert(xml.contains(s"$scmUrl")) assert(xml.contains(s"$scmConnection")) assert(xml.contains(s"$scmDevConnection")) assert(xml.contains(s"$devId")) assert(xml.contains(s"$devName")) assert(xml.contains(s"$devUrl")) assert(xml.contains(s"$devMail")) assert(xml.contains("xmlns:m=")) } test("ivy omits Maven namespace when there is no scm or developer XML") { val organization = Organization("org.example") val moduleName = ModuleName("demo") val version = "1.0" val licenseName = "MIT" val licenseUrl = "https://opensource.org/licenses/MIT" val fixedTime = LocalDateTime.of(2024, 1, 2, 3, 4, 5) val xml = Ivy.create( organization = organization, moduleName = moduleName, version = version, license = Some(Pom.License(licenseName, licenseUrl)), pomProjectName = None, packaging = None, scm = Some(Pom.Scm("", "", "")), developers = Nil, time = fixedTime ) assert(!xml.contains("xmlns:m=")) assert(xml.contains(s""" Right(None) case Some(rawEntryContent) => key.parse(rawEntryContent) .left.map { e => new ConfigDb.ConfigDbFormatError(s"Error parsing ${key.fullName} value", Some(e)) } .map(Some(_)) } /** Sets an entry in memory */ def set[T](key: Key[T], value: T): this.type = { val b = key.write(value) rawEntries += key.fullName -> b this } /** Removes an entry from memory */ def remove(key: Key[?]): this.type = { rawEntries -= key.fullName this } /** Gets an entry in printable form. * * See [[get]] for when a left value, or a None on the right, can be returned. */ def getAsString[T](key: Key[T]): Either[ConfigDb.ConfigDbFormatError, Option[Seq[String]]] = get(key).map(_.map(key.asString)) /** Sets an entry in memory, from a printable / user-writable representation. */ def setFromString[T]( key: Key[T], values: Seq[String] ): Either[Key.MalformedValue, this.type] = key.fromString(values).map { typedValue => set(key, typedValue) } /** Dumps this DB content as JSON */ def dump: Array[Byte] = { def serializeMap(m: Map[String, Array[Byte]], level: Int): Array[Byte] = { val keyValues = m .groupBy(_._1.split("\\.", 2).apply(0)) .toVector .sortBy(_._1) .map { case (k, v) => val v0 = v.map { case (k1, v1) => (k1.stripPrefix(k).stripPrefix("."), v1) } (k, serialize(v0, level + 1)) } val sortedMap: Map[String, RawJson] = ListMap.empty ++ keyValues val b = writeToArray(sortedMap, WriterConfig.withIndentionStep((level + 1) * 2))(using ConfigDb.codec ) if (b.nonEmpty && b.last == '}'.toByte) // FIXME We're copying / moving arrays around quite a bit here b.init ++ (" " * level).getBytes(StandardCharsets.US_ASCII) ++ Array('}'.toByte) else b } def serialize(m: Map[String, Array[Byte]], level: Int): RawJson = m.get("") match { case Some(value) => if (m.size == 1) RawJson(value) else sys.error(s"Inconsistent keys: ${m.keySet.toVector.sorted}") case None => RawJson(serializeMap(m, level)) } serializeMap(rawEntries, level = 0) ++ // using just '\n' rather then "\r\n" on Windows, as that's what jsoniter-scala uses Array('\n': Byte) } private def saveUnsafe(path: Path): Either[ConfigDb.ConfigDbPermissionsError, Unit] = { val dir = path.getParent if (Properties.isWin) { Files.createDirectories(dir) Files.write(path, dump) Right(()) } else { if (!Files.exists(dir)) Files.createDirectories( dir, PosixFilePermissions.asFileAttribute(Set( PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE ).asJava) ) val dirPerms = Files.getPosixFilePermissions(dir).asScala.toSet val permsOk = !dirPerms.contains(PosixFilePermission.GROUP_READ) && !dirPerms.contains(PosixFilePermission.GROUP_WRITE) && !dirPerms.contains(PosixFilePermission.GROUP_EXECUTE) && !dirPerms.contains(PosixFilePermission.OTHERS_READ) && !dirPerms.contains(PosixFilePermission.OTHERS_WRITE) && !dirPerms.contains(PosixFilePermission.OTHERS_EXECUTE) if (permsOk) { Files.write(path, Array.emptyByteArray) Files.setPosixFilePermissions( path, Set( PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE ).asJava ) Files.write(path, dump) Right(()) } else Left(new ConfigDb.ConfigDbPermissionsError(dir, dirPerms)) } } /** Saves this DB at the passed path */ def save(path: Path): Either[Exception, Unit] = // no file locks… saveUnsafe(path) } object ConfigDb { final class ConfigDbFormatError( message: String, causeOpt: Option[Throwable] = None ) extends Exception(message, causeOpt.orNull) private def permsString(perms: Set[PosixFilePermission]): String = { val res = new StringBuilder res += (if (perms.contains(PosixFilePermission.OWNER_READ)) 'r' else '-') res += (if (perms.contains(PosixFilePermission.OWNER_WRITE)) 'w' else '-') res += (if (perms.contains(PosixFilePermission.OWNER_EXECUTE)) 'x' else '-') res += (if (perms.contains(PosixFilePermission.GROUP_READ)) 'r' else '-') res += (if (perms.contains(PosixFilePermission.GROUP_WRITE)) 'w' else '-') res += (if (perms.contains(PosixFilePermission.GROUP_EXECUTE)) 'x' else '-') res += (if (perms.contains(PosixFilePermission.OTHERS_READ)) 'r' else '-') res += (if (perms.contains(PosixFilePermission.OTHERS_WRITE)) 'w' else '-') res += (if (perms.contains(PosixFilePermission.OTHERS_EXECUTE)) 'x' else '-') res.result() } private final class ConfigDbPermissionsError(path: Path, perms: Set[PosixFilePermission]) extends Exception( s"$path has wrong permissions ${permsString(perms)} (expected at most rwx------)" ) private val codec: JsonValueCodec[Map[String, RawJson]] = JsonCodecMaker.make /** Create a ConfigDb instance from binary content * * @param dbContent: * JSON, as a UTF-8 array of bytes * @param printablePath: * DB location, for error messages * @return * either an error on failure, or a ConfigDb instance on success */ def apply( dbContent: Array[Byte], printablePath: Option[String] = None ): Either[ConfigDbFormatError, ConfigDb] = { def flatten(map: Map[String, RawJson]): Map[String, Array[Byte]] = map.flatMap { case (k, v) => try { val subMap = flatten(readFromArray(v.value)(using codec)) subMap.toSeq.map { case (k0, v0) => (k + "." + k0, v0) } } catch { case _: JsonReaderException => Seq(k -> v.value) } } val maybeRawEntries = try Right(flatten(readFromArray(dbContent)(using codec))) catch { case e: JsonReaderException => Left(new ConfigDbFormatError( "Error parsing config DB" + printablePath.fold("")(" " + _), Some(e) )) } maybeRawEntries.map(rawEntries => new ConfigDb(rawEntries)) } /** Creates a ConfigDb from a file * * @param path: * path to a config UTF-8 JSON file * @return * either an error on failure, or a ConfigDb instance on success */ def open(path: Path): Either[Exception, ConfigDb] = if Files.exists(path) then apply(Files.readAllBytes(path), Some(path.toString)) else Right(empty) def empty: ConfigDb = new ConfigDb(Map()) } ================================================ FILE: modules/config/src/main/scala/scala/cli/config/CredentialsValue.scala ================================================ package scala.cli.config trait CredentialsValue { def host: String def user: Option[PasswordOption] def password: Option[PasswordOption] def realm: Option[String] def httpsOnly: Option[Boolean] def asString: String } abstract class CredentialsAsJson[T <: CredentialsValue] { def user: Option[String] def password: Option[String] def toCredentialsValue(userOpt: Option[PasswordOption], passwordOpt: Option[PasswordOption]): T def credentialsType: String private def malformedMessage(valueType: String): String = s"Malformed $credentialsType credentials $valueType value (expected 'value:…', or 'file:/path', or 'env:ENV_VAR_NAME')" def credentials: Either[::[String], T] = { val maybeUser = user .map { u => PasswordOption.parse(u) match { case Left(error) => Left(s"${malformedMessage("user")}: $error") case Right(value) => Right(Some(value)) } } .getOrElse(Right(None)) val maybePassword = password .filter(_.nonEmpty) .map { p => PasswordOption.parse(p) match { case Left(error) => Left(s"${malformedMessage("password")}: $error") case Right(value) => Right(Some(value)) } } .getOrElse(Right(None)) (maybeUser, maybePassword) match { case (Right(userOpt), Right(passwordOpt)) => Right(toCredentialsValue(userOpt, passwordOpt)) case _ => val errors = maybeUser.left.toOption.toList ::: maybePassword.left.toOption.toList match { case Nil => sys.error("Cannot happen") case h :: t => ::(h, t) } Left(errors) } } } ================================================ FILE: modules/config/src/main/scala/scala/cli/config/ErrorMessages.scala ================================================ package scala.cli.config object ErrorMessages { val inlineCredentialsError = "Inline credentials not accepted, please edit the config file manually." } ================================================ FILE: modules/config/src/main/scala/scala/cli/config/Key.scala ================================================ package scala.cli.config import com.github.plokhotnyuk.jsoniter_scala.core.* import com.github.plokhotnyuk.jsoniter_scala.macros.* import scala.cli.commands.SpecificationLevel import scala.cli.config.PublishCredentialsAsJson.* import scala.cli.config.RepositoryCredentialsAsJson.* /** A configuration key */ abstract class Key[T] { /** Key prefix, such as "foo.a" in "foo.a.b" */ def prefix: Seq[String] /** Key name, such as "b" in "foo.a.b" */ def name: String /** Try to parse a value of this key */ def parse(json: Array[Byte]): Either[Key.EntryError, T] /** Converts a value of this key to JSON * * @return * UTF-8 encoded JSON */ def write(value: T): Array[Byte] /** Converts a value of this key to a sequence of strings * * Such a sequence can be printed in the console, and converted back to a [[T]] with * [[fromString]]. */ def asString(value: T): Seq[String] /** Reads a value of this key from a sequence of string */ def fromString(values: Seq[String]): Either[Key.MalformedValue, T] /** The fully qualified name of this key */ final def fullName: String = (prefix :+ name).mkString(".") /** A short description of a particular key's purpose and syntax for its values. */ def description: String /** A flag indicating whether the key should by default be hidden in help outputs or not. */ def hidden: Boolean = false /** Whether this key corresponds to a password (see [[Key.PasswordEntry]]) */ def isPasswordOption: Boolean = false /** The [[SpecificationLevel]] of the key. [[SpecificationLevel.RESTRICTED]] && * [[SpecificationLevel.EXPERIMENTAL]] keys are only available in `power` mode. */ def specificationLevel: SpecificationLevel def isExperimental: Boolean = specificationLevel == SpecificationLevel.EXPERIMENTAL def isRestricted: Boolean = specificationLevel == SpecificationLevel.RESTRICTED } object Key { private implicit lazy val stringJsonCodec: JsonValueCodec[String] = JsonCodecMaker.make private implicit lazy val stringListJsonCodec: JsonValueCodec[List[String]] = JsonCodecMaker.make private implicit lazy val booleanJsonCodec: JsonValueCodec[Boolean] = JsonCodecMaker.make abstract class EntryError( message: String, causeOpt: Option[Throwable] = None ) extends Exception(message, causeOpt.orNull) private final class JsonReaderError(cause: JsonReaderException) extends EntryError("Error parsing config JSON", Some(cause)) final class MalformedValue( entry: Key[?], input: Seq[String], messageOrExpectedShape: Either[String, String], cause: Option[Throwable] = None ) extends EntryError( { val valueWord = if (input.length > 1) "values" else "value" val valuesString = input.map(s => s"'$s'").mkString(", ") val errorMessage = messageOrExpectedShape .fold(shape => s", expected $shape", errorMessage => s". $errorMessage") s"Malformed $valueWord $valuesString for the '${entry.fullName}' entry$errorMessage" }, cause ) private final class MalformedEntry( entry: Key[?], messages: ::[String] ) extends EntryError( s"Malformed entry ${entry.fullName}, " + messages.mkString(", ") ) abstract class KeyWithJsonCodec[T](implicit jsonCodec: JsonValueCodec[T]) extends Key[T] { def parse(json: Array[Byte]): Either[Key.EntryError, T] = try Right(readFromArray(json)) catch { case e: JsonReaderException => Left(new Key.JsonReaderError(e)) } def write(value: T): Array[Byte] = writeToArray(value) } final class StringEntry( val prefix: Seq[String], val name: String, override val specificationLevel: SpecificationLevel, val description: String = "", override val hidden: Boolean = false ) extends KeyWithJsonCodec[String] { def asString(value: String): Seq[String] = Seq(value) def fromString(values: Seq[String]): Either[MalformedValue, String] = values match { case Seq(value) => Right(value) case _ => Left(new MalformedValue(this, values, Left("a single string value."))) } } final class BooleanEntry( val prefix: Seq[String], val name: String, override val specificationLevel: SpecificationLevel, val description: String = "", override val hidden: Boolean = false ) extends KeyWithJsonCodec[Boolean] { def asString(value: Boolean): Seq[String] = Seq(value.toString) def fromString(values: Seq[String]): Either[MalformedValue, Boolean] = values match { case Seq(value) if value.toBooleanOption.isDefined => Right(value.toBoolean) case _ => Left(new MalformedValue( this, values, Left("a single boolean value ('true' or 'false').") )) } } final class PasswordEntry( val prefix: Seq[String], val name: String, override val specificationLevel: SpecificationLevel, val description: String = "", override val hidden: Boolean = false ) extends Key[PasswordOption] { def parse(json: Array[Byte]): Either[EntryError, PasswordOption] = try { val str = readFromArray(json)(using stringJsonCodec) PasswordOption.parse(str).left.map { e => new MalformedValue(this, Seq(str), Right(e)) } } catch { case e: JsonReaderException => Left(new JsonReaderError(e)) } def write(value: PasswordOption): Array[Byte] = writeToArray(value.asString.value)(using stringJsonCodec) def asString(value: PasswordOption): Seq[String] = Seq(value.asString.value) def fromString(values: Seq[String]): Either[MalformedValue, PasswordOption] = values match { case Seq(value) => PasswordOption.parse(value).left.map { err => new MalformedValue(this, values, Right(err)) } case _ => Left(new MalformedValue( this, values, Left("a single password value (format: 'value:password').") )) } override def isPasswordOption: Boolean = true } final class StringListEntry( val prefix: Seq[String], val name: String, override val specificationLevel: SpecificationLevel, val description: String = "", override val hidden: Boolean = false ) extends KeyWithJsonCodec[List[String]] { def asString(value: List[String]): Seq[String] = value def fromString(values: Seq[String]): Either[MalformedValue, List[String]] = Right(values.toList) } abstract class CredentialsEntry[T <: CredentialsValue, U <: CredentialsAsJson[T]](implicit jsonCodec: JsonValueCodec[List[U]] ) extends Key[List[T]] { protected def asJson(credentials: T): U def parse(json: Array[Byte]): Either[Key.EntryError, List[T]] = try { val list = readFromArray(json).map(_.credentials) val errors = list.collect { case Left(errors) => errors }.flatten errors match { case Nil => Right(list.collect { case Right(v) => v }) case h :: t => Left(new Key.MalformedEntry(this, ::(h, t))) } } catch { case e: JsonReaderException => Left(new Key.JsonReaderError(e)) } def write(value: List[T]): Array[Byte] = writeToArray(value.map(asJson)) def fromString(values: Seq[String]): Either[MalformedValue, List[T]] = Left(new Key.MalformedValue(this, values, Right(ErrorMessages.inlineCredentialsError))) def asString(value: List[T]): Seq[String] = value.map(_.asString) } final class RepositoryCredentialsEntry( val prefix: Seq[String], val name: String, override val specificationLevel: SpecificationLevel, val description: String = "", override val hidden: Boolean = false ) extends CredentialsEntry[RepositoryCredentials, RepositoryCredentialsAsJson] { def asJson(credentials: RepositoryCredentials): RepositoryCredentialsAsJson = RepositoryCredentialsAsJson( credentials.host, credentials.user.map(_.asString.value), credentials.password.map(_.asString.value), credentials.realm, credentials.optional, credentials.matchHost, credentials.httpsOnly, credentials.passOnRedirect ) override def asString(value: List[RepositoryCredentials]): Seq[String] = value .zipWithIndex .map { case (cred, idx) => val prefix = s"configRepo$idx." cred.asString.linesWithSeparators.map(prefix + _).mkString } } class PublishCredentialsEntry( val prefix: Seq[String], val name: String, override val specificationLevel: SpecificationLevel, val description: String = "", override val hidden: Boolean = false ) extends CredentialsEntry[PublishCredentials, PublishCredentialsAsJson] { def asJson(credentials: PublishCredentials): PublishCredentialsAsJson = PublishCredentialsAsJson( credentials.host, credentials.user.map(_.asString.value), credentials.password.map(_.asString.value), credentials.realm, credentials.httpsOnly ) } } ================================================ FILE: modules/config/src/main/scala/scala/cli/config/Keys.scala ================================================ package scala.cli.config import scala.cli.commands.SpecificationLevel object Keys { val userName = new Key.StringEntry( prefix = Seq("publish", "user"), name = "name", specificationLevel = SpecificationLevel.EXPERIMENTAL, description = "The 'name' user detail, used for publishing." ) val userEmail = new Key.StringEntry( prefix = Seq("publish", "user"), name = "email", specificationLevel = SpecificationLevel.EXPERIMENTAL, description = "The 'email' user detail, used for publishing." ) val userUrl = new Key.StringEntry( prefix = Seq("publish", "user"), name = "url", specificationLevel = SpecificationLevel.EXPERIMENTAL, description = "The 'url' user detail, used for publishing." ) val ghToken = new Key.PasswordEntry( prefix = Seq("github"), name = "token", specificationLevel = SpecificationLevel.EXPERIMENTAL, description = "GitHub token." ) val pgpSecretKey = new Key.PasswordEntry( prefix = Seq("pgp"), name = "secret-key", specificationLevel = SpecificationLevel.EXPERIMENTAL, description = "The PGP secret key, used for signing." ) val pgpSecretKeyPassword = new Key.PasswordEntry( prefix = Seq("pgp"), name = "secret-key-password", specificationLevel = SpecificationLevel.EXPERIMENTAL, description = "The PGP secret key password, used for signing." ) val pgpPublicKey = new Key.PasswordEntry( prefix = Seq("pgp"), name = "public-key", specificationLevel = SpecificationLevel.EXPERIMENTAL, description = "The PGP public key, used for signing.", hidden = true ) val actions = new Key.BooleanEntry( prefix = Seq.empty, name = "actions", specificationLevel = SpecificationLevel.IMPLEMENTATION, description = "Globally enables actionable diagnostics. Enabled by default." ) val interactive = new Key.BooleanEntry( prefix = Seq.empty, name = "interactive", specificationLevel = SpecificationLevel.IMPLEMENTATION, description = "Globally enables interactive mode (the '--interactive' flag)." ) val power = new Key.BooleanEntry( prefix = Seq.empty, name = "power", specificationLevel = SpecificationLevel.MUST, description = "Globally enables power mode (the '--power' launcher flag)." ) val offline = new Key.BooleanEntry( prefix = Seq.empty, name = "offline", specificationLevel = SpecificationLevel.IMPLEMENTATION, description = "Globally enables offline mode (the '--offline' flag)." ) val suppressDirectivesInMultipleFilesWarning = new Key.BooleanEntry( prefix = Seq("suppress-warning"), name = "directives-in-multiple-files", specificationLevel = SpecificationLevel.IMPLEMENTATION, description = "Globally suppresses warnings about directives declared in multiple source files." ) val suppressOutdatedDependenciessWarning = new Key.BooleanEntry( prefix = Seq("suppress-warning"), name = "outdated-dependencies-files", specificationLevel = SpecificationLevel.IMPLEMENTATION, description = "Globally suppresses warnings about outdated dependencies." ) val suppressExperimentalFeatureWarning = new Key.BooleanEntry( prefix = Seq("suppress-warning"), name = "experimental-features", specificationLevel = SpecificationLevel.IMPLEMENTATION, description = "Globally suppresses warnings about experimental features." ) val suppressDeprecatedFeatureWarning = new Key.BooleanEntry( prefix = Seq("suppress-warning"), name = "deprecated-features", specificationLevel = SpecificationLevel.IMPLEMENTATION, description = "Globally suppresses warnings about deprecated features." ) val proxyAddress = new Key.StringEntry( prefix = Seq("httpProxy"), name = "address", specificationLevel = SpecificationLevel.RESTRICTED, description = "HTTP proxy address." ) val proxyUser = new Key.PasswordEntry( prefix = Seq("httpProxy"), name = "user", specificationLevel = SpecificationLevel.RESTRICTED, description = "HTTP proxy user (used for authentication)." ) val proxyPassword = new Key.PasswordEntry( prefix = Seq("httpProxy"), name = "password", specificationLevel = SpecificationLevel.RESTRICTED, description = "HTTP proxy password (used for authentication)." ) val repositoryMirrors = new Key.StringListEntry( prefix = Seq("repositories"), name = "mirrors", description = s"Repository mirrors, syntax: repositories.mirrors maven:*=https://repository.company.com/maven", specificationLevel = SpecificationLevel.RESTRICTED ) val defaultRepositories = new Key.StringListEntry( prefix = Seq("repositories"), name = "default", description = "Default repository, syntax: https://first-repo.company.com https://second-repo.company.com", specificationLevel = SpecificationLevel.RESTRICTED ) val javaProperties = new Key.StringListEntry( prefix = Nil, name = "java.properties", description = "Java properties for Scala CLI's execution.", specificationLevel = SpecificationLevel.SHOULD ) // Kept for binary compatibility val repositoriesMirrors: Key.StringListEntry = repositoryMirrors // setting indicating if the global interactive mode was suggested val globalInteractiveWasSuggested = new Key.BooleanEntry( prefix = Seq.empty, name = "interactive-was-suggested", specificationLevel = SpecificationLevel.IMPLEMENTATION, description = "Setting indicating if the global interactive mode was already suggested.", hidden = true ) val repositoryCredentials: Key.RepositoryCredentialsEntry = new Key.RepositoryCredentialsEntry( prefix = Seq("repositories"), name = "credentials", specificationLevel = SpecificationLevel.RESTRICTED, description = "Repository credentials, syntax: repositoryAddress value:user value:password [realm]" ) val publishCredentials: Key.PublishCredentialsEntry = new Key.PublishCredentialsEntry( prefix = Seq("publish"), name = "credentials", specificationLevel = SpecificationLevel.EXPERIMENTAL, description = "Publishing credentials, syntax: repositoryAddress value:user value:password [realm]" ) def all: Seq[Key[?]] = Seq[Key[?]]( actions, defaultRepositories, ghToken, globalInteractiveWasSuggested, interactive, javaProperties, suppressDirectivesInMultipleFilesWarning, suppressOutdatedDependenciessWarning, suppressExperimentalFeatureWarning, suppressDeprecatedFeatureWarning, offline, pgpPublicKey, pgpSecretKey, pgpSecretKeyPassword, power, proxyAddress, proxyPassword, proxyUser, publishCredentials, repositoryCredentials, repositoryMirrors, userEmail, userName, userUrl ) lazy val map: Map[String, Key[?]] = all.map(e => e.fullName -> e).toMap } ================================================ FILE: modules/config/src/main/scala/scala/cli/config/PasswordOption.scala ================================================ package scala.cli.config import com.github.plokhotnyuk.jsoniter_scala.core.* import com.github.plokhotnyuk.jsoniter_scala.macros.* import java.io.ByteArrayOutputStream import java.nio.charset.StandardCharsets import java.nio.file.{Files, Path, Paths} sealed abstract class PasswordOption extends Product with Serializable { def get(): Secret[String] def getBytes: Secret[Array[Byte]] = get().map(_.getBytes(StandardCharsets.UTF_8)) def asString: Secret[String] } abstract class LowPriorityPasswordOption { protected lazy val commandCodec: JsonValueCodec[List[String]] = JsonCodecMaker.make def parse(str: String): Either[String, PasswordOption] = if (str.startsWith("value:")) Right(PasswordOption.Value(Secret(str.stripPrefix("value:")))) else if (str.startsWith("file:")) Right(PasswordOption.File(Paths.get(str.stripPrefix("file:")))) else if (str.startsWith("env:")) Right(PasswordOption.Env(str.stripPrefix("env:"))) else if (str.startsWith("command:[")) try { val command = readFromString(str.stripPrefix("command:"))(using commandCodec) Right(PasswordOption.Command(command)) } catch { case e: JsonReaderException => Left(s"Error decoding password command: ${e.getMessage}") } else if (str.startsWith("command:")) { val command = str.stripPrefix("command:").split("\\s+").toSeq Right(PasswordOption.Command(command)) } else Left("Malformed password value (expected \"value:password\")") } object PasswordOption extends LowPriorityPasswordOption { final case class Value(value: Secret[String]) extends PasswordOption { def get(): Secret[String] = value def asString: Secret[String] = get().map(v => s"value:$v") } final case class Env(name: String) extends PasswordOption { def get(): Secret[String] = { val value = Option(System.getenv(name)).getOrElse { sys.error(s"Error: environment variable $name not set") } Secret(value) } def asString: Secret[String] = Secret(s"env:$name") } final case class File(path: Path) extends PasswordOption { def get(): Secret[String] = { val value = new String(Files.readAllBytes(path), StandardCharsets.UTF_8) // trim that? Secret(value) } override def getBytes: Secret[Array[Byte]] = { val value = Files.readAllBytes(path) Secret(value) } def asString: Secret[String] = Secret(s"file:$path") } final case class Command(command: Seq[String]) extends PasswordOption { def get(): Secret[String] = { // should we add a timeout? // Better use ProcessBuilder than sys.process here, as the latter // adds superfluous new line characters when reading the command output. // That way, we know we are reading the actual output of the command, and we // don't have to speculatively trim the result (which would work if the new // line is added by sys.process, but wouldn't if the new line is legit and is in // the original output). val b = new ProcessBuilder(command*) b.redirectInput(ProcessBuilder.Redirect.INHERIT) b.redirectError(ProcessBuilder.Redirect.INHERIT) b.redirectOutput(ProcessBuilder.Redirect.PIPE) val p = b.start() val is = p.getInputStream var read = -1 val buf = Array.ofDim[Byte](2048) val baos = new ByteArrayOutputStream while ({ read = is.read(buf) read >= 0 }) if (read > 0) baos.write(buf, 0, read) val exitCode = p.waitFor() if (exitCode != 0) throw new RuntimeException( s"Error running command ${command.mkString(" ")} (exit code: $exitCode)" ) val res = new String(baos.toByteArray) // Using default codec here Secret(res) } def asString: Secret[String] = { val json = writeToString(command.toList)(using commandCodec) Secret(s"command:$json") } } } ================================================ FILE: modules/config/src/main/scala/scala/cli/config/PublishCredentials.scala ================================================ package scala.cli.config import com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker final case class PublishCredentials( host: String = "", user: Option[PasswordOption] = None, password: Option[PasswordOption] = None, realm: Option[String] = None, httpsOnly: Option[Boolean] = None ) extends CredentialsValue { override def asString: String = { val prefix = httpsOnly match { case Some(true) => "https://" case Some(false) => "http://" case None => "//" } // FIXME We're getting secrets and putting them in a non-Secret guarded string here val credentialsPart = { val realmPart = realm.map("(" + _ + ")").getOrElse("") val userPart = user.map(_.get().value).getOrElse("") val passwordPart = password.map(":" + _.get().value).getOrElse("") if (realmPart.nonEmpty || userPart.nonEmpty || passwordPart.nonEmpty) realmPart + userPart + passwordPart + "@" else "" } prefix + credentialsPart + host } } final case class PublishCredentialsAsJson( host: String, user: Option[String] = None, password: Option[String] = None, realm: Option[String] = None, httpsOnly: Option[Boolean] = None ) extends CredentialsAsJson[PublishCredentials] { def credentialsType: String = "publish" def toCredentialsValue( userOpt: Option[PasswordOption], passwordOpt: Option[PasswordOption] ): PublishCredentials = PublishCredentials( host = host, user = userOpt, password = passwordOpt, realm = realm, httpsOnly = httpsOnly ) } object PublishCredentialsAsJson { implicit lazy val listJsonCodec: JsonValueCodec[List[PublishCredentialsAsJson]] = JsonCodecMaker.make } ================================================ FILE: modules/config/src/main/scala/scala/cli/config/RawJson.scala ================================================ package scala.cli.config import com.github.plokhotnyuk.jsoniter_scala.core.* import java.nio.charset.StandardCharsets import java.util import scala.util.Try import scala.util.hashing.MurmurHash3 // adapted from https://github.com/plokhotnyuk/jsoniter-scala/blob/209d918a030b188f064ee55505a6c47257731b4b/jsoniter-scala-macros/src/test/scala/com/github/plokhotnyuk/jsoniter_scala/macros/JsonCodecMakerSpec.scala#L645-L666 private[config] final case class RawJson(value: Array[Byte]) { override lazy val hashCode: Int = MurmurHash3.arrayHash(value) override def equals(obj: Any): Boolean = obj match { case that: RawJson => util.Arrays.equals(value, that.value) case _ => false } override def toString: String = Try(new String(value, StandardCharsets.UTF_8)) .toOption .getOrElse(value.toString) } private[config] object RawJson { implicit val codec: JsonValueCodec[RawJson] = new JsonValueCodec[RawJson] { def decodeValue(in: JsonReader, default: RawJson): RawJson = new RawJson(in.readRawValAsBytes()) def encodeValue(x: RawJson, out: JsonWriter): Unit = out.writeRawVal(x.value) val nullValue: RawJson = new RawJson(new Array[Byte](0)) } val emptyObj: RawJson = RawJson("{}".getBytes(StandardCharsets.UTF_8)) } ================================================ FILE: modules/config/src/main/scala/scala/cli/config/RepositoryCredentials.scala ================================================ package scala.cli.config import com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker import scala.collection.mutable.ListBuffer final case class RepositoryCredentials( host: String = "", user: Option[PasswordOption] = None, password: Option[PasswordOption] = None, realm: Option[String] = None, optional: Option[Boolean] = None, matchHost: Option[Boolean] = None, httpsOnly: Option[Boolean] = None, passOnRedirect: Option[Boolean] = None ) extends CredentialsValue { def asString: String = { val lines = new ListBuffer[String] if (host.nonEmpty) lines += s"host=$host" for (u <- user) lines += s"username=${u.asString.value}" for (p <- password) lines += s"password=${p.asString.value}" for (r <- realm) lines += s"realm=$r" for (b <- httpsOnly) lines += s"https-only=$b" for (b <- matchHost) lines += s"auto=$b" for (b <- passOnRedirect) lines += s"pass-on-redirect=$b" // seems cred.optional can't be changed from properties… lines.map(_ + System.lineSeparator()).mkString } } final case class RepositoryCredentialsAsJson( host: String, user: Option[String] = None, password: Option[String] = None, realm: Option[String] = None, optional: Option[Boolean] = None, matchHost: Option[Boolean] = None, httpsOnly: Option[Boolean] = None, passOnRedirect: Option[Boolean] = None ) extends CredentialsAsJson[RepositoryCredentials] { def credentialsType: String = "repository" def toCredentialsValue( userOpt: Option[PasswordOption], passwordOpt: Option[PasswordOption] ): RepositoryCredentials = RepositoryCredentials( host = host, user = userOpt, password = passwordOpt, realm = realm, optional = optional, matchHost = matchHost, httpsOnly = httpsOnly, passOnRedirect = passOnRedirect ) } object RepositoryCredentialsAsJson { implicit lazy val listJsonCodec: JsonValueCodec[List[RepositoryCredentialsAsJson]] = JsonCodecMaker.make } ================================================ FILE: modules/config/src/main/scala/scala/cli/config/Secret.scala ================================================ package scala.cli.config final class Secret[+T]( value0: T ) { def value: T = value0 def map[U](f: T => U): Secret[U] = Secret(f(value0)) override def equals(obj: Any): Boolean = obj match { case other: Secret[_] => value == other.value case _ => false } // not leaking details about the secret here override def hashCode(): Int = 0 override def toString: String = "****" } object Secret { def apply[T](value: T): Secret[T] = new Secret(value) } ================================================ FILE: modules/config/src/main/scala/scala/cli/config/internal/JavaHelper.scala ================================================ package scala.cli.config.internal import java.lang.Boolean as JBoolean import java.nio.file.Path import scala.cli.commands.SpecificationLevel import scala.cli.config.* object JavaHelper { private var dbOpt = Option.empty[ConfigDb] def open(dbPath: Path): Unit = if (dbOpt.isEmpty) { val db0 = ConfigDb.open(dbPath) match { case Left(ex) => throw new Exception(ex) case Right(db1) => db1 } dbOpt = Some(db0) } def close(): Unit = if (dbOpt.nonEmpty) dbOpt = None private def split(key: String): (Seq[String], String) = { val elems = key.split("\\.") (elems.init.toSeq, elems.last) } def getString(key: String): String = { val db = dbOpt.getOrElse(sys.error("DB not open")) val (prefix, name) = split(key) val key0 = new Key.StringEntry(prefix, name, SpecificationLevel.IMPLEMENTATION) db.get(key0) match { case Left(ex) => throw new Exception(ex) case Right(None) => null case Right(Some(str)) => str } } def getBoolean(key: String): JBoolean = { val db = dbOpt.getOrElse(sys.error("DB not open")) val (prefix, name) = split(key) val key0 = new Key.BooleanEntry(prefix, name, SpecificationLevel.IMPLEMENTATION) db.get(key0) match { case Left(ex) => throw new Exception(ex) case Right(None) => null case Right(Some(value)) => value } } def getStringList(key: String): Array[String] = { val db = dbOpt.getOrElse(sys.error("DB not open")) val (prefix, name) = split(key) val key0 = new Key.StringListEntry(prefix, name, SpecificationLevel.IMPLEMENTATION) db.get(key0) match { case Left(ex) => throw new Exception(ex) case Right(None) => null case Right(Some(value)) => value.toArray } } def getPassword(key: String): String = { val db = dbOpt.getOrElse(sys.error("DB not open")) val (prefix, name) = split(key) val key0 = new Key.PasswordEntry(prefix, name, SpecificationLevel.IMPLEMENTATION) db.get(key0) match { case Left(ex) => throw new Exception(ex) case Right(None) => null case Right(Some(str)) => str.get().value } } def getPasswordBytes(key: String): Array[Byte] = { val db = dbOpt.getOrElse(sys.error("DB not open")) val (prefix, name) = split(key) val key0 = new Key.PasswordEntry(prefix, name, SpecificationLevel.IMPLEMENTATION) db.get(key0) match { case Left(ex) => throw new Exception(ex) case Right(None) => null case Right(Some(str)) => str.getBytes.value } } } ================================================ FILE: modules/core/src/main/scala/scala/build/CsUtils.scala ================================================ package scala.build import coursier.version.Version import scala.build.internal.Constants extension (s: String) def coursierVersion: Version = Version(s) extension (csv: Version) def isScala38OrNewer: Boolean = Constants.scala38Versions .map(_.coursierVersion) .exists(_ <= csv) ================================================ FILE: modules/core/src/main/scala/scala/build/Logger.scala ================================================ package scala.build import bloop.rifle.BloopRifleLogger import org.scalajs.logging.{Logger as ScalaJsLogger, NullLogger} import java.io.{OutputStream, PrintStream} import scala.annotation.unused import scala.build.errors.{BuildException, Diagnostic, Severity} import scala.build.internals.FeatureType import scala.scalanative.build as sn trait Logger { def error(message: String): Unit // TODO Use macros for log and debug calls to have zero cost when verbosity <= 0 def message(message: => String): Unit def log(s: => String): Unit def log(s: => String, debug: => String): Unit def debug(s: => String): Unit def debugStackTrace(t: => Throwable): Unit = t.getStackTrace.foreach(ste => debug(ste.toString)) def log(diagnostics: Seq[Diagnostic]): Unit def diagnostic( message: String, severity: Severity = Severity.Warning, positions: Seq[Position] = Nil ): Unit = log(Seq(Diagnostic(message, severity, positions))) def log(ex: BuildException): Unit def debug(ex: BuildException): Unit def exit(ex: BuildException): Nothing def coursierLogger(printBefore: String): coursier.cache.CacheLogger def bloopRifleLogger: BloopRifleLogger def scalaJsLogger: ScalaJsLogger def scalaNativeTestLogger: sn.Logger def scalaNativeCliInternalLoggerOptions: List[String] def compilerOutputStream: PrintStream def verbosity: Int /** Since we have a lot of experimental warnings all over the build process, this method can be * used to accumulate them for a better UX */ def experimentalWarning(featureName: String, featureType: FeatureType): Unit def flushExperimentalWarnings: Unit def cliFriendlyDiagnostic( message: String, @unused cliFriendlyMessage: String, severity: Severity = Severity.Warning, positions: Seq[Position] = Nil ): Unit = diagnostic(message, severity, positions) } object Logger { private class Nop extends Logger { def error(message: String): Unit = () def message(message: => String): Unit = () def log(s: => String): Unit = () def log(s: => String, debug: => String): Unit = () def debug(s: => String): Unit = () def log(diagnostics: Seq[Diagnostic]): Unit = () def log(ex: BuildException): Unit = () def debug(ex: BuildException): Unit = () def exit(ex: BuildException): Nothing = throw new Exception(ex) def coursierLogger(printBefore: String): coursier.cache.CacheLogger = coursier.cache.CacheLogger.nop def bloopRifleLogger: BloopRifleLogger = BloopRifleLogger.nop def scalaJsLogger: ScalaJsLogger = NullLogger def scalaNativeTestLogger: sn.Logger = sn.Logger.nullLogger def scalaNativeCliInternalLoggerOptions: List[String] = List() def compilerOutputStream: PrintStream = new PrintStream( new OutputStream { override def write(b: Int): Unit = () override def write(b: Array[Byte], off: Int, len: Int): Unit = () } ) def verbosity: Int = -1 def experimentalWarning(featureUsed: String, featureType: FeatureType): Unit = () def flushExperimentalWarnings: Unit = () } def nop: Logger = new Nop } ================================================ FILE: modules/core/src/main/scala/scala/build/Os.scala ================================================ package scala.build import java.util.Locale import scala.util.Properties object Os { lazy val pwd: os.Path = if (Properties.isWin) os.Path(os.pwd.toIO.getCanonicalFile) else os.pwd lazy val isArmArchitecture: Boolean = sys.props.getOrElse("os.arch", "").toLowerCase(Locale.ROOT) == "aarch64" } ================================================ FILE: modules/core/src/main/scala/scala/build/Position.scala ================================================ package scala.build import scala.collection.mutable sealed abstract class Position { def render(): String = render(Os.pwd, java.io.File.separator) def render(cwd: os.Path): String = render(cwd, java.io.File.separator) def render(cwd: os.Path, sep: String): String } object Position { final case class File( path: Either[String, os.Path], startPos: (Int, Int), endPos: (Int, Int), offset: Int = 0 ) extends Position { def render(cwd: os.Path, sep: String): String = { val p = path match { case Left(p0) => p0 case Right(p0) => if (p0.startsWith(cwd)) p0.relativeTo(cwd).segments.mkString(sep) else p0.toString } if (startPos == endPos) s"$p:${startPos._1 + 1}:${startPos._2 + 1}" else if (startPos._1 == endPos._1) s"$p:${startPos._1 + 1}:${startPos._2 + 1}-${endPos._2 + 1}" else s"$p:${startPos._1 + 1}:${startPos._2 + 1}-${endPos._1 + 1}:${endPos._2 + 1}" } } final case class Raw(startIdx: Int, endIdx: Int) extends Position { def render(cwd: os.Path, sep: String): String = s"raw $startIdx:$endIdx" def +(shift: Int): Raw = Raw(startIdx + shift, endIdx + shift) } object Raw { // from https://github.com/com-lihaoyi/Ammonite/blob/76673f7f3eb9d9ae054482635f57a31527d248de/amm/interp/src/main/scala/ammonite/interp/script/PositionOffsetConversion.scala#L7-L69 def lineStartIndices(content: String): Array[Int] = { val content0 = content.toCharArray // adapted from scala/scala SourceFile.scala val length = content0.length val CR = '\r' val LF = '\n' def charAtIsEOL(idx: Int)(p: Char => Boolean) = { // don't identify the CR in CR LF as a line break, since LF will do. def notCRLF0 = content0(idx) != CR || !content0.isDefinedAt(idx + 1) || content0(idx + 1) != LF idx < length && notCRLF0 && p(content0(idx)) } def isAtEndOfLine(idx: Int) = charAtIsEOL(idx) { case CR | LF => true case _ => false } val buf = new mutable.ArrayBuffer[Int] buf += 0 for (i <- 0 until content0.length if isAtEndOfLine(i)) buf += i + 1 buf.toArray } private def offsetToPos(content: String): Int => (Int, Int) = { val lineStartIndices0 = lineStartIndices(content) def offsetToLine(offset: Int): Int = { assert(lineStartIndices0.nonEmpty) if (offset >= lineStartIndices0.last) lineStartIndices0.length - 1 else { def find(a: Int, b: Int): Int = if (a + 1 >= b) a else { val c = (a + b) / 2 val idx = lineStartIndices0(c) if (idx == offset) c else if (idx < offset) find(c, b) else find(a, c) } find(0, lineStartIndices0.length - 1) } } offset => assert(offset >= 0) assert(offset <= content.length) val line = offsetToLine(offset) (line, offset - lineStartIndices0(line)) } def filePos(path: Either[String, os.Path], content: String): Raw => File = { val f = offsetToPos(content) raw => val startPos = f(raw.startIdx) val endPos = f(raw.endIdx) File(path, startPos, endPos) } } final case class CommandLine(arg: String = "") extends Position { // todo the exact arg should be somehow taken from CaseApp def render(cwd: os.Path, sep: String): String = "COMMAND_LINE" } final case class Bloop(bloopJavaPath: String) extends Position { def render(cwd: os.Path, sep: String): String = bloopJavaPath } final case class Custom(msg: String) extends Position { def render(cwd: os.Path, sep: String): String = msg } } ================================================ FILE: modules/core/src/main/scala/scala/build/RepositoryUtils.scala ================================================ package scala.build import coursier.maven.MavenRepository object RepositoryUtils { lazy val snapshotsRepositoryUrl = "https://central.sonatype.com/repository/maven-snapshots" lazy val snapshotsRepository = MavenRepository(snapshotsRepositoryUrl) lazy val scala3NightlyRepositoryUrl = "https://repo.scala-lang.org/artifactory/maven-nightlies" lazy val scala3NightlyRepository = MavenRepository(scala3NightlyRepositoryUrl) } ================================================ FILE: modules/core/src/main/scala/scala/build/errors/AmbiguousPlatformError.scala ================================================ package scala.build.errors final class AmbiguousPlatformError(passedPlatformTypes: Seq[String]) extends BuildException( s"Ambiguous platform: more than one type of platform has been passed: ${passedPlatformTypes.mkString(", ")}." ) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/BuildException.scala ================================================ package scala.build.errors import scala.build.Position abstract class BuildException( override val message: String, override val positions: Seq[Position] = Nil, cause: Throwable = null ) extends Exception(message, cause) with Diagnostic { final override def severity: Severity = Severity.Error /** @param default * default value returned as a [[Right]] instance on recovery * @param maybeRecoverFunction * potential recovery function, returns [[None]] on recovery and Some(identity(_)) otherwise * @tparam T * type of the default value * @return * Right(default) on recovery, Left(buildException) otherwise */ def maybeRecoverWithDefault[T]( default: T, maybeRecoverFunction: BuildException => Option[BuildException] ): Either[BuildException, T] = maybeRecoverFunction(this) match { case Some(e) => Left(e) case None => Right(default) } } ================================================ FILE: modules/core/src/main/scala/scala/build/errors/BuildInfoGenerationError.scala ================================================ package scala.build.errors import scala.build.Position final class BuildInfoGenerationError(msg: String, positions: Seq[Position], cause: Exception) extends BuildException( s"BuildInfo generation error: $msg", positions = positions, cause = cause ) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/CantDownloadAmmoniteError.scala ================================================ package scala.build.errors import coursier.error.ResolutionError import scala.build.Position final class CantDownloadAmmoniteError( ammoniteVersion: String, scalaVersion: String, underlying: ResolutionError.CantDownloadModule, override val positions: Seq[Position] ) extends BuildException( s"""Can't download Ammonite $ammoniteVersion for Scala $scalaVersion. |Ammonite with this Scala version might not yet be supported. |Try passing a different Scala version with the -S option.""".stripMargin, positions, underlying ) object CantDownloadAmmoniteError { def apply( ammoniteVersion: String, scalaVersion: String, underlying: ResolutionError.CantDownloadModule, positions: Seq[Position] ): CantDownloadAmmoniteError = new CantDownloadAmmoniteError(ammoniteVersion, scalaVersion, underlying, positions) } ================================================ FILE: modules/core/src/main/scala/scala/build/errors/CheckScalaCliVersionError.scala ================================================ package scala.build.errors class CheckScalaCliVersionError(val message: String, val cause: Throwable) extends Exception(message, cause) object CheckScalaCliVersionError { def apply(message: String, cause: Throwable = null): CheckScalaCliVersionError = new CheckScalaCliVersionError(message, cause) } ================================================ FILE: modules/core/src/main/scala/scala/build/errors/CompositeBuildException.scala ================================================ package scala.build.errors final class CompositeBuildException private ( val mainException: BuildException, val others: Seq[BuildException] ) extends BuildException( s"${others.length + 1} exceptions, first one: ${mainException.getMessage}", Nil, mainException ) { def exceptions: Seq[BuildException] = mainException +: others } object CompositeBuildException { private def flatten(list: ::[BuildException]): ::[BuildException] = { val list0 = list.flatMap { case c: CompositeBuildException => c.mainException :: c.others.toList case e => e :: Nil } list0 match { case Nil => sys.error("Can't happen") case h :: t => ::(h, t) } } def apply(exceptions: ::[BuildException]): BuildException = flatten(exceptions) match { case h :: Nil => h case h :: t => new CompositeBuildException(h, t) } def apply(exceptions: Seq[BuildException]): BuildException = exceptions.distinct match { case Seq(head) => head case head +: tail => new CompositeBuildException(head, tail) } } ================================================ FILE: modules/core/src/main/scala/scala/build/errors/ConfigDbException.scala ================================================ package scala.build.errors final class ConfigDbException(cause: Exception) extends BuildException(s"Config DB error: ${cause.getMessage}", cause = cause) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/CoursierDependencyError.scala ================================================ package scala.build.errors import coursier.error.DependencyError import scala.build.Position class CoursierDependencyError(val underlying: DependencyError, positions: Seq[Position] = Seq.empty) extends BuildException( s"Could not fetch dependency: ${underlying.message}", positions = positions, cause = underlying.getCause ) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/DependencyFormatError.scala ================================================ package scala.build.errors import scala.build.Position final class DependencyFormatError( val dependencyString: String, val error: String, positions: Seq[Position] ) extends BuildException( s"Error parsing dependency '$dependencyString': $error", positions = positions ) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/Diagnostic.scala ================================================ package scala.build.errors import scala.build.Position import scala.build.errors.Diagnostic.TextEdit trait Diagnostic { def message: String def severity: Severity def positions: Seq[Position] def textEdit: Option[TextEdit] = None } object Diagnostic { case class TextEdit(title: String, newText: String) object Messages { val bloopTooOld = """JVM that is hosting bloop is older than the requested runtime. Please restart the Build Server from your IDE. |Or run the command `bloop exit`, and then use `--jvm` flag to request a sufficient JVM version. |""".stripMargin } private case class ADiagnostic( message: String, severity: Severity, positions: Seq[Position], override val textEdit: Option[TextEdit] ) extends Diagnostic def apply( message: String, severity: Severity, positions: Seq[Position] = Nil, textEdit: Option[TextEdit] = None ): Diagnostic = ADiagnostic(message, severity, positions, textEdit) } ================================================ FILE: modules/core/src/main/scala/scala/build/errors/DirectiveErrors.scala ================================================ package scala.build.errors import scala.build.Position final class DirectiveErrors(errors: ::[String], positions: Seq[Position]) extends BuildException( "Directives errors: " + errors.mkString(", "), positions = positions ) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/ExcludeDefinitionError.scala ================================================ package scala.build.errors import scala.build.Position final class ExcludeDefinitionError(positions: Seq[Position], expectedProjectFilePath: os.Path) extends BuildException( s"""Found exclude directives in files: | ${positions.map(_.render()).distinct.mkString(", ")} |exclude directive must be defined in project configuration file: $expectedProjectFilePath.""".stripMargin ) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/FetchingDependenciesError.scala ================================================ package scala.build.errors import coursier.error.CoursierError import scala.build.Position final class FetchingDependenciesError( val underlying: CoursierError, override val positions: Seq[Position] ) extends BuildException( underlying.getMessage, positions, underlying ) object FetchingDependenciesError { def unapply(e: FetchingDependenciesError): Option[(CoursierError, Seq[Position])] = Some(e.underlying -> e.positions) } ================================================ FILE: modules/core/src/main/scala/scala/build/errors/FileNotFoundException.scala ================================================ package scala.build.errors final class FileNotFoundException(val path: os.Path) extends BuildException(s"File not found: $path") ================================================ FILE: modules/core/src/main/scala/scala/build/errors/ForbiddenPathReferenceError.scala ================================================ package scala.build.errors import scala.build.Position final class ForbiddenPathReferenceError( val virtualRoot: String, val positionOpt: Option[Position] ) extends BuildException( s"Can't reference paths from sources from $virtualRoot", positionOpt.toSeq ) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/InputsException.scala ================================================ package scala.build.errors class InputsException(message: String) extends BuildException( message = message ) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/InvalidBinaryScalaVersionError.scala ================================================ package scala.build.errors final class InvalidBinaryScalaVersionError(val invalidBinaryVersion: String) extends ScalaVersionError(s"Cannot find matching Scala version for '$invalidBinaryVersion'") ================================================ FILE: modules/core/src/main/scala/scala/build/errors/JmhBuildFailedError.scala ================================================ package scala.build.errors final class JmhBuildFailedError extends BuildException("JMH build failed") ================================================ FILE: modules/core/src/main/scala/scala/build/errors/JvmDownloadError.scala ================================================ package scala.build.errors final class JvmDownloadError(jvmId: String, cause: Throwable) extends BuildException( s"Cannot download JVM: $jvmId", cause = cause ) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/MainClassError.scala ================================================ package scala.build.errors import scala.build.Position abstract class MainClassError( message: String, positions: Seq[Position] ) extends BuildException(message, positions = positions) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/MalformedCliInputError.scala ================================================ package scala.build.errors final class MalformedCliInputError(message: String) extends BuildException(message) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/MalformedDirectiveError.scala ================================================ package scala.build.errors import scala.build.Position final class MalformedDirectiveError(message: String, positions: Seq[Position]) extends BuildException(message, positions) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/MalformedInputError.scala ================================================ package scala.build.errors import scala.build.Position final class MalformedInputError( val inputType: String, val input: String, val expectedShape: String, positions: Seq[Position] = Nil, cause: Option[Throwable] = None ) extends BuildException( { val q = "\"" s"Malformed $inputType $q$input$q, expected $expectedShape" }, positions = positions, cause = cause.orNull ) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/MalformedPlatformError.scala ================================================ package scala.build.errors import scala.build.Position final class MalformedPlatformError( marformedInput: String, positions: Seq[Position] = Nil ) extends BuildException( s"Unrecognized platform: $marformedInput", positions = positions ) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/MarkdownUnclosedBackticksError.scala ================================================ package scala.build.errors import scala.build.Position class MarkdownUnclosedBackticksError( backticks: String, positions: Seq[Position] ) extends BuildException(s"Unclosed $backticks code block in a Markdown input", positions) object MarkdownUnclosedBackticksError { def apply(backticks: String, positions: Seq[Position]) = new MarkdownUnclosedBackticksError(backticks, positions) } ================================================ FILE: modules/core/src/main/scala/scala/build/errors/ModuleFormatError.scala ================================================ package scala.build.errors import scala.build.Position final class ModuleFormatError( val moduleString: String, val error: String, val originOpt: Option[String] = None, positions: Seq[Position] = Nil ) extends BuildException( s"Error parsing ${originOpt.getOrElse("")}module '$moduleString': $error", positions = positions ) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/MultipleScalaVersionsError.scala ================================================ package scala.build.errors final class MultipleScalaVersionsError(scalaVersions: Seq[String]) extends BuildException( message = s"Multiple Scala versions are present in the build (${scalaVersions.mkString(" ")}), even though only one is allowed in this context.", positions = Nil ) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/NoDocBuildError.scala ================================================ package scala.build.errors final class NoDocBuildError extends BuildException( "Doc build not present. It may have been cancelled." ) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/NoFrameworkFoundByBridgeError.scala ================================================ package scala.build.errors final class NoFrameworkFoundByBridgeError extends TestError("No framework found by Scala.js test bridge") ================================================ FILE: modules/core/src/main/scala/scala/build/errors/NoFrameworkFoundByNativeBridgeError.scala ================================================ package scala.build.errors final class NoFrameworkFoundByNativeBridgeError extends TestError("No framework found by Scala Native test bridge") ================================================ FILE: modules/core/src/main/scala/scala/build/errors/NoMainClassFoundError.scala ================================================ package scala.build.errors final class NoMainClassFoundError extends MainClassError("No main class found", Nil) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/NoScalaVersionProvidedError.scala ================================================ package scala.build.errors import scala.build.Position final class NoScalaVersionProvidedError( val depOrModule: Either[dependency.AnyModule, dependency.AnyDependency], positions: Seq[Position] = Nil ) extends BuildException( { val str = depOrModule match { case Left(mod) => "module " + mod.render case Right(dep) => "dependency " + dep.render } s"Got Scala $str, but no Scala version is provided" }, positions = positions ) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/NoTestFrameworkFoundError.scala ================================================ package scala.build.errors final class NoTestFrameworkFoundError extends TestError("No test framework found") ================================================ FILE: modules/core/src/main/scala/scala/build/errors/NoTestFrameworkValueProvidedError.scala ================================================ package scala.build.errors final class NoTestFrameworkValueProvidedError extends BuildException( "No test framework value provided to using test-framework directive" ) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/NoTestsRun.scala ================================================ package scala.build.errors final class NoTestsRun extends TestError("No tests were run") ================================================ FILE: modules/core/src/main/scala/scala/build/errors/NoValidScalaVersionFoundError.scala ================================================ package scala.build.errors final class NoValidScalaVersionFoundError(val versionString: String = "") extends ScalaVersionError({ val suffix = if versionString.nonEmpty then s" for $versionString" else "" s"Cannot find a valid matching Scala version$suffix." }) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/NoValueProvidedError.scala ================================================ package scala.build.errors final class NoValueProvidedError(val key: String) extends BuildException( s"Expected a value for directive $key", // TODO - this seems like outdated thing positions = Nil // I wish using_directives provided the key position… ) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/NodeNotFoundError.scala ================================================ package scala.build.errors final class NodeNotFoundError extends BuildException( "The Node was not found on the PATH. Please ensure that Node is installed correctly and then try again" ) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/ParsingInputsException.scala ================================================ package scala.build.errors class ParsingInputsException(exceptionMessage: String, cause: Throwable) extends BuildException( message = exceptionMessage, cause = cause ) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/RepositoryFormatError.scala ================================================ package scala.build.errors final class RepositoryFormatError(errors: ::[String]) extends BuildException( s"Error parsing repositories: ${errors.mkString(", ")}" ) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/ScalaNativeBuildError.scala ================================================ package scala.build.errors final class ScalaNativeBuildError() extends BuildException(s"Error compiling with Scala Native") ================================================ FILE: modules/core/src/main/scala/scala/build/errors/ScalaNativeCompatibilityError.scala ================================================ package scala.build.errors final class ScalaNativeCompatibilityError( val scalaVersion: String, val scalaNativeVersion: String ) extends BuildException( s"""Used Scala Native version $scalaNativeVersion is incompatible with Scala $scalaVersion. |Please try one of the following combinations: | Scala Native version >= 0.4.4 for Scala 3.1 (*.sc & *.scala files) | Scala Native version >= 0.4.0 for Scala 2.13 (*.sc & *.scala files) | Scala Native version >= 0.4.0 for Scala 2.12 (*.scala files) |Windows is supported since Scala Native 0.4.1. |""".stripMargin ) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/ScalaVersionError.scala ================================================ package scala.build.errors import scala.build.Position import scala.build.internal.Constants.* class ScalaVersionError(message: String, positions: Seq[Position] = Nil, cause: Throwable = null) extends BuildException( s"$message${ScalaVersionError.getTheGeneralErrorInfo}", positions = positions, cause ) {} object ScalaVersionError { private lazy val defaultScalaVersions = Seq(defaultScala212Version, defaultScala213Version, defaultScalaVersion) lazy val getTheGeneralErrorInfo: String = s""" |You can only choose one of the 3.x, 2.13.x, and 2.12.x. versions. |The latest supported stable versions are ${defaultScalaVersions.mkString(", ")}. |In addition, you can request compilation with the last nightly versions of Scala, |by passing the 2.nightly, 2.12.nightly, 2.13.nightly, or 3.nightly arguments. |Specific Scala 2 or Scala 3 nightly versions are also accepted. |You can also request the latest Scala 3 LTS by passing lts or 3.lts. |""".stripMargin } ================================================ FILE: modules/core/src/main/scala/scala/build/errors/ScalafixPropertiesError.scala ================================================ package scala.build.errors final class ScalafixPropertiesError( path: os.Path, cause: Option[Throwable] = None ) extends BuildException( message = { val causeMessage = cause.map(c => s": ${c.getMessage}").getOrElse("") s"Failed to load Scalafix properties at $path$causeMessage" }, cause = cause.orNull ) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/SeveralMainClassesFoundError.scala ================================================ package scala.build.errors import scala.build.Position final class SeveralMainClassesFoundError( mainClasses: ::[String], commandString: String, positions: Seq[Position] ) extends MainClassError( s"""Found several main classes: ${mainClasses.mkString(", ")} |You can run one of them by passing it with the --main-class option, e.g. | ${Console.BOLD}$commandString --main-class ${mainClasses.head}${Console.RESET} | |You can pick the main class interactively by passing the --interactive option. | ${Console.BOLD}$commandString --interactive${Console.RESET}""".stripMargin, positions = positions ) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/Severity.scala ================================================ package scala.build.errors import ch.epfl.scala.bsp4j as b sealed abstract class Severity extends Product with Serializable { def toBsp4j: b.DiagnosticSeverity } object Severity { case object Error extends Severity { override def toBsp4j: b.DiagnosticSeverity = b.DiagnosticSeverity.ERROR } case object Warning extends Severity { override def toBsp4j: b.DiagnosticSeverity = b.DiagnosticSeverity.WARNING } case object Hint extends Severity { override def toBsp4j: b.DiagnosticSeverity = b.DiagnosticSeverity.HINT } } ================================================ FILE: modules/core/src/main/scala/scala/build/errors/TestError.scala ================================================ package scala.build.errors abstract class TestError(message: String) extends BuildException(message) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/TooManyFrameworksFoundByBridgeError.scala ================================================ package scala.build.errors final class TooManyFrameworksFoundByBridgeError extends TestError("Too many frameworks found by Scala.js test bridge") ================================================ FILE: modules/core/src/main/scala/scala/build/errors/ToolkitVersionError.scala ================================================ package scala.build.errors import scala.build.Position final class ToolkitVersionError(msg: String, positions: Seq[Position]) extends BuildException(msg, positions) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/UnexpectedDirectiveError.scala ================================================ package scala.build.errors final class UnexpectedDirectiveError(val key: String) extends BuildException(s"Unexpected directive: $key}") ================================================ FILE: modules/core/src/main/scala/scala/build/errors/UnexpectedJvmPlatformVersionError.scala ================================================ package scala.build.errors import scala.build.Position final class UnexpectedJvmPlatformVersionError( version: String, positions: Seq[Position] ) extends BuildException( s"Unexpected version '$version' specified for JVM platform", positions = positions ) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/UnrecognizedDebugModeError.scala ================================================ package scala.build.errors final class UnrecognizedDebugModeError(mode: String) extends BuildException( s"Unrecognized debug mode: $mode." ) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/UnrecognizedJsOptModeError.scala ================================================ package scala.build.errors final class UnrecognizedJsOptModeError( mode: String, aliasesForFullLink: Seq[String], aliasesForFastLink: Seq[String] ) extends BuildException( s"""Unrecognized JS optimization mode: $mode. |Available options: |- for fastLinkJs: ${aliasesForFastLink.mkString(", ")} |- for fullLinkJs: ${aliasesForFullLink.mkString(", ")}""".stripMargin ) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/UnsupportedFeatureError.scala ================================================ package scala.build.errors import scala.build.Position abstract class UnsupportedFeatureError( val featureDescription: String, override val positions: Seq[Position] = Nil ) extends BuildException(s"Unsupported feature: $featureDescription") ================================================ FILE: modules/core/src/main/scala/scala/build/errors/UnsupportedGradleModuleVariantError.scala ================================================ package scala.build.errors import coursier.core.VariantPublication import scala.build.Position class UnsupportedGradleModuleVariantError( val variantPublication: VariantPublication, override val positions: Seq[Position] = Nil ) extends UnsupportedFeatureError(featureDescription = s"Gradle Module variant: ${variantPublication.name} (${variantPublication.url})" ) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/UnsupportedScalaVersionError.scala ================================================ package scala.build.errors final class UnsupportedScalaVersionError(val binaryVersion: String) extends ScalaVersionError(s"Unsupported Scala version: $binaryVersion") ================================================ FILE: modules/core/src/main/scala/scala/build/errors/UnusedDirectiveError.scala ================================================ package scala.build.errors import scala.build.Position final class UnusedDirectiveError(key: String, values: Seq[String], position: Position) extends BuildException( s"Unrecognized directive: $key${ if values.isEmpty then "" else s" with values: ${values.mkString(", ")}" }", positions = List(position) ) ================================================ FILE: modules/core/src/main/scala/scala/build/errors/WorkspaceError.scala ================================================ package scala.build.errors final class WorkspaceError(message: String) extends BuildException(message) ================================================ FILE: modules/core/src/main/scala/scala/build/internals/CodeWrapper.scala ================================================ package scala.build.internal abstract class CodeWrapper { def apply( code: String, pkgName: Seq[Name], indexedWrapperName: Name, extraCode: String, scriptPath: String ): (String, String) def mainClassObject(className: Name): Name def wrapCode( pkgName: Seq[Name], indexedWrapperName: Name, code: String, scriptPath: String ): (String, WrapperParams) = { // we need to normalize topWrapper and bottomWrapper in order to ensure // the snippets always use the platform-specific newLine val extraCode0 = "/**/" val (topWrapper, bottomWrapper) = apply(code, pkgName, indexedWrapperName, extraCode0, scriptPath) // match lineSeparator to existing code val nl = code.indexOf("\n") match { case n if n > 0 && code(n - 1) == '\r' => System.lineSeparator() case _ => "\n" } val (topWrapper0, bottomWrapper0) = ( topWrapper + "/**/ /**/" + bottomWrapper ) val mainClassName = (pkgName :+ mainClassObject(indexedWrapperName)).map( _.encoded ).mkString(".") val wrapperParams = WrapperParams(topWrapper0.linesIterator.size, code.linesIterator.size, mainClassName) (topWrapper0 + code + bottomWrapper0, wrapperParams) } } case class WrapperParams(topWrapperLineCount: Int, userCodeLineCount: Int, mainClass: String) ================================================ FILE: modules/core/src/main/scala/scala/build/internals/ConsoleUtils.scala ================================================ package scala.build.internals object ConsoleUtils { import Console.* object ScalaCliConsole { lazy val warnPrefix = s"[${YELLOW}warn$RESET]" val GRAY: String = "\u001b[90m" } val ansiFormattingKeys: Set[String] = Set(RESET, BOLD, UNDERLINED, REVERSED, INVISIBLE) val ansiColors: Set[String] = Set(BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, ScalaCliConsole.GRAY) val ansiBoldColors: Set[String] = Set(BLACK_B, RED_B, GREEN_B, YELLOW_B, BLUE_B, MAGENTA_B, CYAN_B, WHITE_B) val allAnsiColors: Set[String] = ansiColors ++ ansiBoldColors val allConsoleKeys: Set[String] = allAnsiColors ++ ansiFormattingKeys extension (s: String) { def noConsoleKeys: String = allConsoleKeys.fold(s)((acc, consoleKey) => acc.replace(consoleKey, "")) } } ================================================ FILE: modules/core/src/main/scala/scala/build/internals/CsLoggerUtil.scala ================================================ package scala.build.internal import coursier.cache.FileCache import coursier.cache.loggers.{ProgressBarRefreshDisplay, RefreshDisplay, RefreshLogger} import coursier.jvm.JavaHome import coursier.util.Task object CsLoggerUtil { // All of these methods are a bit flaky… private lazy val loggerDisplay: RefreshLogger => RefreshDisplay = { val m = classOf[RefreshLogger].getDeclaredField("display") m.setAccessible(true) logger => m.get(logger).asInstanceOf[RefreshDisplay] } implicit class CsCacheExtensions(private val cache: FileCache[Task]) extends AnyVal { def withMessage(message: String): FileCache[Task] = cache.logger match { case logger: RefreshLogger => val shouldUpdateLogger = loggerDisplay(logger) match { case _: CustomProgressBarRefreshDisplay => true case _: ProgressBarRefreshDisplay => true case _ => false } if (shouldUpdateLogger) { var displayed = false val updatedLogger = RefreshLogger.create( CustomProgressBarRefreshDisplay.create( keepOnScreen = false, if (!displayed) { System.err.println(message) displayed = true }, () ) ) updatedLogger.init() cache.withLogger(updatedLogger) } else cache case _ => cache } } implicit class CsJavaHomeExtensions(private val javaHome: JavaHome) extends AnyVal { def withMessage(message: String): JavaHome = javaHome.cache.map(_.archiveCache.cache) match { case Some(f: FileCache[Task]) => val cache0 = f.withMessage(message) javaHome.withCache( javaHome.cache.map(c => c.withArchiveCache(c.archiveCache.withCache(cache0))) ) case _ => javaHome } } } ================================================ FILE: modules/core/src/main/scala/scala/build/internals/CustomProgressBarRefreshDisplay.scala ================================================ package scala.build.internal // Same as ProgressBarRefreshDisplay in coursier, but allowing not to keep progress // bars on screen import coursier.cache.internal.ConsoleDim import coursier.cache.loggers.* import java.io.Writer import java.sql.Timestamp import scala.concurrent.duration.{Duration, DurationInt} class CustomProgressBarRefreshDisplay( /** Whether to keep details on screen after this display is stopped */ keepOnScreen: Boolean, beforeOutput: => Unit, afterOutput: => Unit ) extends RefreshDisplay { import coursier.cache.internal.Terminal.Ansi import CustomProgressBarRefreshDisplay.display val refreshInterval: Duration = 20.millis private var printedAnything0 = false private var currentHeight = 0 override def stop(out: Writer): Unit = { for (_ <- 1 to 2; _ <- 0 until currentHeight) { out.clearLine(2) out.down(1) } for (_ <- 0 until currentHeight) out.up(2) out.flush() if (printedAnything0) { afterOutput printedAnything0 = false } currentHeight = 0 } private def truncatedPrintln(out: Writer, s: String, width: Int): Unit = { out.clearLine(2) out.write(RefreshDisplay.truncated(s, width)) out.write('\n') } def update( out: Writer, done: Seq[(String, RefreshInfo)], downloads: Seq[(String, RefreshInfo)], changed: Boolean ): Unit = if (changed) { val width = ConsoleDim.width() val done0 = done .filter { case (url, _) => !url.endsWith(".sha1") && !url.endsWith(".sha256") && !url.endsWith(".md5") && !url.endsWith("/") } val elems = done0.iterator.map((_, true)) ++ downloads.iterator.map((_, false)) for (((url, info), isDone) <- elems) { assert(info != null, s"Incoherent state ($url)") if (!printedAnything0) { beforeOutput printedAnything0 = true } truncatedPrintln(out, url, width) out.clearLine(2) out.write(s" ${display(info, isDone)}" + System.lineSeparator()) } val displayedCount = done0.length + downloads.length if (displayedCount < currentHeight) { for (_ <- 1 to 2; _ <- displayedCount until currentHeight) { out.clearLine(2) out.down(1) } for (_ <- displayedCount until currentHeight) out.up(2) } for (_ <- downloads.indices) out.up(2) if (!keepOnScreen) for (_ <- done0.indices) out.up(2) out.left(10000) out.flush() currentHeight = if (keepOnScreen) downloads.length else displayedCount } } object CustomProgressBarRefreshDisplay { def create(): CustomProgressBarRefreshDisplay = new CustomProgressBarRefreshDisplay(keepOnScreen = true, (), ()) def create( beforeOutput: => Unit, afterOutput: => Unit ): CustomProgressBarRefreshDisplay = new CustomProgressBarRefreshDisplay(keepOnScreen = true, beforeOutput, afterOutput) def create( keepOnScreen: Boolean, beforeOutput: => Unit, afterOutput: => Unit ): CustomProgressBarRefreshDisplay = new CustomProgressBarRefreshDisplay(keepOnScreen, beforeOutput, afterOutput) // Scala version of http://stackoverflow.com/questions/3758606/how-to-convert-byte-size-into-human-readable-format-in-java/3758880#3758880 def byteCount(bytes: Long, si: Boolean = false) = { val unit = if (si) 1000 else 1024 if (bytes < unit) bytes.toString + "B" else { val prefixes = if (si) "kMGTPE" else "KMGTPE" val exp = (math.log(bytes.toDouble) / math.log(unit.toDouble)).toInt min prefixes.length val pre = prefixes.charAt(exp - 1).toString + (if (si) "" else "i") f"${bytes / math.pow(unit.toDouble, exp.toDouble)}%.1f ${pre}B" } } private val format = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss") private def formatTimestamp(ts: Long): String = format.format(new Timestamp(ts)) private def display(info: RefreshInfo, isDone: Boolean): String = info match { case d: RefreshInfo.DownloadInfo => val actualFraction = d.fraction .orElse(if (isDone) Some(1.0) else None) .orElse(if (d.downloaded == 0L) Some(0.0) else None) val start = actualFraction match { case None => " [ ] " case Some(frac) => val elem = if (d.watching) "." else "#" val decile = (10.0 * frac).toInt assert(decile >= 0) assert(decile <= 10) f"${100.0 * frac}%5.1f%%" + " [" + (elem * decile) + (" " * (10 - decile)) + "] " } start + byteCount(d.downloaded) + d.rate().fold("")(r => s" (${byteCount(r.toLong)} / s)") case c: RefreshInfo.CheckUpdateInfo => if (isDone) (c.currentTimeOpt, c.remoteTimeOpt) match { case (Some(current), Some(remote)) => if (current < remote) s"Updated since ${formatTimestamp(current)} (${formatTimestamp(remote)})" else if (current == remote) s"No new update since ${formatTimestamp(current)}" else s"Warning: local copy newer than remote one (${formatTimestamp(current)} > ${formatTimestamp(remote)})" case (Some(_), None) => // FIXME Likely a 404 Not found, that should be taken into account by the cache "No modified time in response" case (None, Some(remote)) => s"Last update: ${formatTimestamp(remote)}" case (None, None) => "" // ??? } else c.currentTimeOpt match { case Some(current) => s"Checking for updates since ${formatTimestamp(current)}" case None => "" // ??? } } } ================================================ FILE: modules/core/src/main/scala/scala/build/internals/EnvVar.scala ================================================ package scala.build.internals import scala.build.internals.ConsoleUtils.ScalaCliConsole /** @param name * The name of the environment variable * @param description * A short description what is it used for * @param passToIde * Whether to pass this variable to the IDE/BSP client (true by default, should only be disabled * for env vars which aren't safe to save on disk) * @param requiresPower * Whether this variable is related to a feature that requires power mode; also used for internal * toggles and such */ case class EnvVar( name: String, description: String, passToIde: Boolean = true, requiresPower: Boolean = false ) { def valueOpt: Option[String] = Option(System.getenv(name)) override def toString: String = s"$name=${valueOpt.getOrElse("")}" def helpMessage(spaces: String): String = { val powerString = if requiresPower then s"${ScalaCliConsole.GRAY}(power)${Console.RESET} " else "" s"${Console.YELLOW}$name${Console.RESET}$spaces$powerString$description" } } object EnvVar { def helpMessage(isPower: Boolean): String = s"""The following is the list of environment variables used and recognized by Scala CLI. |It should by no means be treated as an exhaustive list |Some tools and libraries Scala CLI integrates with may have their own, which may or may not be listed here. |${if isPower then "" else "For the expanded list, pass --power." + System.lineSeparator()} |${ val maxFullNameLength = EnvVar.all.filter(!_.requiresPower || isPower).map(_.name.length).max EnvVar.allGroups .map(_.subsectionMessage(maxFullNameLength, isPower)) .filter(_.linesIterator.size > 1) .mkString(s"${System.lineSeparator() * 2}") }""".stripMargin trait EnvVarGroup { def all: Seq[EnvVar] def groupName: String def subsectionMessage(maxFullNameLength: Int, isPower: Boolean): String = { val envsToInclude = all.filter(!_.requiresPower || isPower) s"""$groupName |${ envsToInclude .map(ev => s" ${ev.helpMessage(spaces = " " * (maxFullNameLength - ev.name.length + 2))}" ) .mkString(System.lineSeparator()) }""".stripMargin } } def allGroups: Seq[EnvVarGroup] = Seq(ScalaCli, Java, Bloop, Coursier, Spark, Misc, Internal) def all: Seq[EnvVar] = allGroups.flatMap(_.all) def allBsp: Seq[EnvVar] = all.filter(_.passToIde) object Java extends EnvVarGroup { override def groupName: String = "Java" override def all = Seq(javaHome, javaOpts, jdkJavaOpts) val javaHome = EnvVar("JAVA_HOME", "Java installation directory") val javaOpts = EnvVar("JAVA_OPTS", "Java options") val jdkJavaOpts = EnvVar("JDK_JAVA_OPTIONS", "JDK Java options") } object Misc extends EnvVarGroup { override def groupName: String = "Miscellaneous" override def all = Seq( path, dyldLibraryPath, ldLibraryPath, pathExt, shell, vcVarsAll, zDotDir ) val path = EnvVar("PATH", "The app path variable") val dyldLibraryPath = EnvVar("DYLD_LIBRARY_PATH", "Runtime library paths on Mac OS X") val ldLibraryPath = EnvVar("LD_LIBRARY_PATH", "Runtime library paths on Linux") val pathExt = EnvVar("PATHEXT", "Executable file extensions on Windows") val pwd = EnvVar("PWD", "Current working directory", passToIde = false) val shell = EnvVar("SHELL", "The currently used shell") val vcVarsAll = EnvVar("VCVARSALL", "Visual C++ Redistributable Runtimes") val zDotDir = EnvVar("ZDOTDIR", "Zsh configuration directory") val mavenHome = EnvVar("MAVEN_HOME", "Maven home directory") } object Coursier extends EnvVarGroup { override def groupName: String = "Coursier" override def all = Seq( coursierBinDir, coursierCache, coursierConfigDir, coursierCredentials, insideEmacs, coursierExperimental, coursierJni, coursierMode, coursierNoTerm, coursierProgress, coursierRepositories, coursierVendoredZis, csMavenHome ) val coursierBinDir = EnvVar("COURSIER_BIN_DIR", "Coursier app binaries directory") val coursierCache = EnvVar("COURSIER_CACHE", "Coursier cache location") val coursierConfigDir = EnvVar("COURSIER_CONFIG_DIR", "Coursier configuration directory") val coursierCredentials = EnvVar("COURSIER_CREDENTIALS", "Coursier credentials") val coursierExperimental = EnvVar("COURSIER_EXPERIMENTAL", "Experimental mode toggle") val coursierJni = EnvVar("COURSIER_JNI", "Coursier JNI toggle") val coursierMode = EnvVar("COURSIER_MODE", "Coursier mode (can be set to 'offline')") val coursierNoTerm = EnvVar("COURSIER_NO_TERM", "Terminal toggle") val coursierProgress = EnvVar("COURSIER_PROGRESS", "Progress bar toggle") val coursierRepositories = EnvVar("COURSIER_REPOSITORIES", "Coursier repositories") val coursierVendoredZis = EnvVar("COURSIER_VENDORED_ZIS", "Toggle io.github.scala_cli.zip.ZipInputStream") val csMavenHome = EnvVar("CS_MAVEN_HOME", "Coursier Maven home directory") val insideEmacs = EnvVar("INSIDE_EMACS", "Emacs toggle") } object ScalaCli extends EnvVarGroup { override def groupName: String = "Scala CLI" def all = Seq( config, home, interactive, interactiveInputs, power, printStackTraces, allowSodiumJni, vendoredZipInputStream ) val config = EnvVar("SCALA_CLI_CONFIG", "Scala CLI configuration file path") val extraTimeout = Bloop.bloopExtraTimeout.copy(requiresPower = false) val home = EnvVar("SCALA_CLI_HOME", "Scala CLI home directory") val interactive = EnvVar("SCALA_CLI_INTERACTIVE", "Interactive mode toggle") val interactiveInputs = EnvVar("SCALA_CLI_INTERACTIVE_INPUTS", "Interactive mode inputs") val power = EnvVar("SCALA_CLI_POWER", "Power mode toggle") val printStackTraces = EnvVar("SCALA_CLI_PRINT_STACK_TRACES", "Print stack traces toggle") val allowSodiumJni = EnvVar("SCALA_CLI_SODIUM_JNI_ALLOW", "Allow to load libsodiumjni") val vendoredZipInputStream = EnvVar("SCALA_CLI_VENDORED_ZIS", "Toggle io.github.scala_cli.zip.ZipInputStream") } object Spark extends EnvVarGroup { override def groupName: String = "Spark" override def all = Seq(sparkHome) val sparkHome = EnvVar("SPARK_HOME", "Spark installation directory", requiresPower = true) } object Bloop extends EnvVarGroup { override def groupName: String = "Bloop" override def all = Seq( bloopComputationCores, bloopDaemonDir, bloopJavaOpts, bloopModule, bloopPort, bloopScalaVersion, bloopVersion, bloopServer, bloopExtraTimeout ) val bloopComputationCores = EnvVar( "BLOOP_COMPUTATION_CORES", "Number of computation cores to be used", requiresPower = true ) val bloopDaemonDir = EnvVar("BLOOP_DAEMON_DIR", "Bloop daemon directory", requiresPower = true) val bloopJavaOpts = EnvVar("BLOOP_JAVA_OPTS", "Bloop Java options", requiresPower = true) val bloopModule = EnvVar("BLOOP_MODULE", "Bloop default module", requiresPower = true) val bloopPort = EnvVar("BLOOP_PORT", "Bloop default port", requiresPower = true) val bloopScalaVersion = EnvVar("BLOOP_SCALA_VERSION", "Bloop default Scala version", requiresPower = true) val bloopVersion = EnvVar("BLOOP_VERSION", "Bloop default version", requiresPower = true) val bloopServer = EnvVar("BLOOP_SERVER", "Bloop default host", requiresPower = true) val bloopExtraTimeout = EnvVar("SCALA_CLI_EXTRA_TIMEOUT", "Extra timeout", requiresPower = true) } object Internal extends EnvVarGroup { override def groupName: String = "Internal" def all = Seq(ci) val ci = EnvVar("CI", "Marker for running on the CI", requiresPower = true) } } ================================================ FILE: modules/core/src/main/scala/scala/build/internals/FeatureType.scala ================================================ package scala.build.internals enum FeatureType(stringRepr: String) { override def toString: String = stringRepr case Option extends FeatureType("option") case Directive extends FeatureType("directive") case Subcommand extends FeatureType("sub-command") case ConfigKey extends FeatureType("configuration key") } object FeatureType { private val ordering = Map( FeatureType.Subcommand -> 0, FeatureType.Option -> 1, FeatureType.Directive -> 2, FeatureType.ConfigKey -> 3 ) given Ordering[FeatureType] = Ordering.by(ordering) } ================================================ FILE: modules/core/src/main/scala/scala/build/internals/License.scala ================================================ package scala.build.internal final case class License( id: String, name: String, url: String ) ================================================ FILE: modules/core/src/main/scala/scala/build/internals/Licenses.scala ================================================ package scala.build.internal object Licenses { // format: off val list = Seq( License("0BSD", "BSD Zero Clause License", "https://spdx.org/licenses/0BSD.html"), License("AAL", "Attribution Assurance License", "https://spdx.org/licenses/AAL.html"), License("ADSL", "Amazon Digital Services License", "https://spdx.org/licenses/ADSL.html"), License("AFL-1.1", "Academic Free License v1.1", "https://spdx.org/licenses/AFL-1.1.html"), License("AFL-1.2", "Academic Free License v1.2", "https://spdx.org/licenses/AFL-1.2.html"), License("AFL-2.0", "Academic Free License v2.0", "https://spdx.org/licenses/AFL-2.0.html"), License("AFL-2.1", "Academic Free License v2.1", "https://spdx.org/licenses/AFL-2.1.html"), License("AFL-3.0", "Academic Free License v3.0", "https://spdx.org/licenses/AFL-3.0.html"), License("AGPL-1.0", "Affero General Public License v1.0", "https://spdx.org/licenses/AGPL-1.0.html"), License("AGPL-1.0-only", "Affero General Public License v1.0 only", "https://spdx.org/licenses/AGPL-1.0-only.html"), License("AGPL-1.0-or-later", "Affero General Public License v1.0 or later", "https://spdx.org/licenses/AGPL-1.0-or-later.html"), License("AGPL-3.0", "GNU Affero General Public License v3.0", "https://spdx.org/licenses/AGPL-3.0.html"), License("AGPL-3.0-only", "GNU Affero General Public License v3.0 only", "https://spdx.org/licenses/AGPL-3.0-only.html"), License("AGPL-3.0-or-later", "GNU Affero General Public License v3.0 or later", "https://spdx.org/licenses/AGPL-3.0-or-later.html"), License("AMDPLPA", "AMD's plpa_map.c License", "https://spdx.org/licenses/AMDPLPA.html"), License("AML", "Apple MIT License", "https://spdx.org/licenses/AML.html"), License("AMPAS", "Academy of Motion Picture Arts and Sciences BSD", "https://spdx.org/licenses/AMPAS.html"), License("ANTLR-PD", "ANTLR Software Rights Notice", "https://spdx.org/licenses/ANTLR-PD.html"), License("ANTLR-PD-fallback", "ANTLR Software Rights Notice with license fallback", "https://spdx.org/licenses/ANTLR-PD-fallback.html"), License("APAFML", "Adobe Postscript AFM License", "https://spdx.org/licenses/APAFML.html"), License("APL-1.0", "Adaptive Public License 1.0", "https://spdx.org/licenses/APL-1.0.html"), License("APSL-1.0", "Apple Public Source License 1.0", "https://spdx.org/licenses/APSL-1.0.html"), License("APSL-1.1", "Apple Public Source License 1.1", "https://spdx.org/licenses/APSL-1.1.html"), License("APSL-1.2", "Apple Public Source License 1.2", "https://spdx.org/licenses/APSL-1.2.html"), License("APSL-2.0", "Apple Public Source License 2.0", "https://spdx.org/licenses/APSL-2.0.html"), License("Abstyles", "Abstyles License", "https://spdx.org/licenses/Abstyles.html"), License("Adobe-2006", "Adobe Systems Incorporated Source Code License Agreement", "https://spdx.org/licenses/Adobe-2006.html"), License("Adobe-Glyph", "Adobe Glyph List License", "https://spdx.org/licenses/Adobe-Glyph.html"), License("Afmparse", "Afmparse License", "https://spdx.org/licenses/Afmparse.html"), License("Aladdin", "Aladdin Free Public License", "https://spdx.org/licenses/Aladdin.html"), License("Apache-1.0", "Apache License 1.0", "https://spdx.org/licenses/Apache-1.0.html"), License("Apache-1.1", "Apache License 1.1", "https://spdx.org/licenses/Apache-1.1.html"), License("Apache-2.0", "Apache License 2.0", "https://spdx.org/licenses/Apache-2.0.html"), License("App-s2p", "App::s2p License", "https://spdx.org/licenses/App-s2p.html"), License("Arphic-1999", "Arphic Public License", "https://spdx.org/licenses/Arphic-1999.html"), License("Artistic-1.0", "Artistic License 1.0", "https://spdx.org/licenses/Artistic-1.0.html"), License("Artistic-1.0-Perl", "Artistic License 1.0 (Perl)", "https://spdx.org/licenses/Artistic-1.0-Perl.html"), License("Artistic-1.0-cl8", "Artistic License 1.0 w/clause 8", "https://spdx.org/licenses/Artistic-1.0-cl8.html"), License("Artistic-2.0", "Artistic License 2.0", "https://spdx.org/licenses/Artistic-2.0.html"), License("BSD-1-Clause", "BSD 1-Clause License", "https://spdx.org/licenses/BSD-1-Clause.html"), License("BSD-2-Clause", "BSD 2-Clause \"Simplified\" License", "https://spdx.org/licenses/BSD-2-Clause.html"), License("BSD-2-Clause-FreeBSD", "BSD 2-Clause FreeBSD License", "https://spdx.org/licenses/BSD-2-Clause-FreeBSD.html"), License("BSD-2-Clause-NetBSD", "BSD 2-Clause NetBSD License", "https://spdx.org/licenses/BSD-2-Clause-NetBSD.html"), License("BSD-2-Clause-Patent", "BSD-2-Clause Plus Patent License", "https://spdx.org/licenses/BSD-2-Clause-Patent.html"), License("BSD-2-Clause-Views", "BSD 2-Clause with views sentence", "https://spdx.org/licenses/BSD-2-Clause-Views.html"), License("BSD-3-Clause", "BSD 3-Clause \"New\" or \"Revised\" License", "https://spdx.org/licenses/BSD-3-Clause.html"), License("BSD-3-Clause-Attribution", "BSD with attribution", "https://spdx.org/licenses/BSD-3-Clause-Attribution.html"), License("BSD-3-Clause-Clear", "BSD 3-Clause Clear License", "https://spdx.org/licenses/BSD-3-Clause-Clear.html"), License("BSD-3-Clause-LBNL", "Lawrence Berkeley National Labs BSD variant license", "https://spdx.org/licenses/BSD-3-Clause-LBNL.html"), License("BSD-3-Clause-Modification", "BSD 3-Clause Modification", "https://spdx.org/licenses/BSD-3-Clause-Modification.html"), License("BSD-3-Clause-No-Military-License", "BSD 3-Clause No Military License", "https://spdx.org/licenses/BSD-3-Clause-No-Military-License.html"), License("BSD-3-Clause-No-Nuclear-License", "BSD 3-Clause No Nuclear License", "https://spdx.org/licenses/BSD-3-Clause-No-Nuclear-License.html"), License("BSD-3-Clause-No-Nuclear-License-2014", "BSD 3-Clause No Nuclear License 2014", "https://spdx.org/licenses/BSD-3-Clause-No-Nuclear-License-2014.html"), License("BSD-3-Clause-No-Nuclear-Warranty", "BSD 3-Clause No Nuclear Warranty", "https://spdx.org/licenses/BSD-3-Clause-No-Nuclear-Warranty.html"), License("BSD-3-Clause-Open-MPI", "BSD 3-Clause Open MPI variant", "https://spdx.org/licenses/BSD-3-Clause-Open-MPI.html"), License("BSD-4-Clause", "BSD 4-Clause \"Original\" or \"Old\" License", "https://spdx.org/licenses/BSD-4-Clause.html"), License("BSD-4-Clause-Shortened", "BSD 4 Clause Shortened", "https://spdx.org/licenses/BSD-4-Clause-Shortened.html"), License("BSD-4-Clause-UC", "BSD-4-Clause (University of California-Specific)", "https://spdx.org/licenses/BSD-4-Clause-UC.html"), License("BSD-Protection", "BSD Protection License", "https://spdx.org/licenses/BSD-Protection.html"), License("BSD-Source-Code", "BSD Source Code Attribution", "https://spdx.org/licenses/BSD-Source-Code.html"), License("BSL-1.0", "Boost Software License 1.0", "https://spdx.org/licenses/BSL-1.0.html"), License("BUSL-1.1", "Business Source License 1.1", "https://spdx.org/licenses/BUSL-1.1.html"), License("Bahyph", "Bahyph License", "https://spdx.org/licenses/Bahyph.html"), License("Barr", "Barr License", "https://spdx.org/licenses/Barr.html"), License("Beerware", "Beerware License", "https://spdx.org/licenses/Beerware.html"), License("BitTorrent-1.0", "BitTorrent Open Source License v1.0", "https://spdx.org/licenses/BitTorrent-1.0.html"), License("BitTorrent-1.1", "BitTorrent Open Source License v1.1", "https://spdx.org/licenses/BitTorrent-1.1.html"), License("BlueOak-1.0.0", "Blue Oak Model License 1.0.0", "https://spdx.org/licenses/BlueOak-1.0.0.html"), License("Borceux", "Borceux license", "https://spdx.org/licenses/Borceux.html"), License("C-UDA-1.0", "Computational Use of Data Agreement v1.0", "https://spdx.org/licenses/C-UDA-1.0.html"), License("CAL-1.0", "Cryptographic Autonomy License 1.0", "https://spdx.org/licenses/CAL-1.0.html"), License("CAL-1.0-Combined-Work-Exception", "Cryptographic Autonomy License 1.0 (Combined Work Exception)", "https://spdx.org/licenses/CAL-1.0-Combined-Work-Exception.html"), License("CATOSL-1.1", "Computer Associates Trusted Open Source License 1.1", "https://spdx.org/licenses/CATOSL-1.1.html"), License("CC-BY-1.0", "Creative Commons Attribution 1.0 Generic", "https://spdx.org/licenses/CC-BY-1.0.html"), License("CC-BY-2.0", "Creative Commons Attribution 2.0 Generic", "https://spdx.org/licenses/CC-BY-2.0.html"), License("CC-BY-2.5", "Creative Commons Attribution 2.5 Generic", "https://spdx.org/licenses/CC-BY-2.5.html"), License("CC-BY-2.5-AU", "Creative Commons Attribution 2.5 Australia", "https://spdx.org/licenses/CC-BY-2.5-AU.html"), License("CC-BY-3.0", "Creative Commons Attribution 3.0 Unported", "https://spdx.org/licenses/CC-BY-3.0.html"), License("CC-BY-3.0-AT", "Creative Commons Attribution 3.0 Austria", "https://spdx.org/licenses/CC-BY-3.0-AT.html"), License("CC-BY-3.0-DE", "Creative Commons Attribution 3.0 Germany", "https://spdx.org/licenses/CC-BY-3.0-DE.html"), License("CC-BY-3.0-NL", "Creative Commons Attribution 3.0 Netherlands", "https://spdx.org/licenses/CC-BY-3.0-NL.html"), License("CC-BY-3.0-US", "Creative Commons Attribution 3.0 United States", "https://spdx.org/licenses/CC-BY-3.0-US.html"), License("CC-BY-4.0", "Creative Commons Attribution 4.0 International", "https://spdx.org/licenses/CC-BY-4.0.html"), License("CC-BY-NC-1.0", "Creative Commons Attribution Non Commercial 1.0 Generic", "https://spdx.org/licenses/CC-BY-NC-1.0.html"), License("CC-BY-NC-2.0", "Creative Commons Attribution Non Commercial 2.0 Generic", "https://spdx.org/licenses/CC-BY-NC-2.0.html"), License("CC-BY-NC-2.5", "Creative Commons Attribution Non Commercial 2.5 Generic", "https://spdx.org/licenses/CC-BY-NC-2.5.html"), License("CC-BY-NC-3.0", "Creative Commons Attribution Non Commercial 3.0 Unported", "https://spdx.org/licenses/CC-BY-NC-3.0.html"), License("CC-BY-NC-3.0-DE", "Creative Commons Attribution Non Commercial 3.0 Germany", "https://spdx.org/licenses/CC-BY-NC-3.0-DE.html"), License("CC-BY-NC-4.0", "Creative Commons Attribution Non Commercial 4.0 International", "https://spdx.org/licenses/CC-BY-NC-4.0.html"), License("CC-BY-NC-ND-1.0", "Creative Commons Attribution Non Commercial No Derivatives 1.0 Generic", "https://spdx.org/licenses/CC-BY-NC-ND-1.0.html"), License("CC-BY-NC-ND-2.0", "Creative Commons Attribution Non Commercial No Derivatives 2.0 Generic", "https://spdx.org/licenses/CC-BY-NC-ND-2.0.html"), License("CC-BY-NC-ND-2.5", "Creative Commons Attribution Non Commercial No Derivatives 2.5 Generic", "https://spdx.org/licenses/CC-BY-NC-ND-2.5.html"), License("CC-BY-NC-ND-3.0", "Creative Commons Attribution Non Commercial No Derivatives 3.0 Unported", "https://spdx.org/licenses/CC-BY-NC-ND-3.0.html"), License("CC-BY-NC-ND-3.0-DE", "Creative Commons Attribution Non Commercial No Derivatives 3.0 Germany", "https://spdx.org/licenses/CC-BY-NC-ND-3.0-DE.html"), License("CC-BY-NC-ND-3.0-IGO", "Creative Commons Attribution Non Commercial No Derivatives 3.0 IGO", "https://spdx.org/licenses/CC-BY-NC-ND-3.0-IGO.html"), License("CC-BY-NC-ND-4.0", "Creative Commons Attribution Non Commercial No Derivatives 4.0 International", "https://spdx.org/licenses/CC-BY-NC-ND-4.0.html"), License("CC-BY-NC-SA-1.0", "Creative Commons Attribution Non Commercial Share Alike 1.0 Generic", "https://spdx.org/licenses/CC-BY-NC-SA-1.0.html"), License("CC-BY-NC-SA-2.0", "Creative Commons Attribution Non Commercial Share Alike 2.0 Generic", "https://spdx.org/licenses/CC-BY-NC-SA-2.0.html"), License("CC-BY-NC-SA-2.0-FR", "Creative Commons Attribution-NonCommercial-ShareAlike 2.0 France", "https://spdx.org/licenses/CC-BY-NC-SA-2.0-FR.html"), License("CC-BY-NC-SA-2.0-UK", "Creative Commons Attribution Non Commercial Share Alike 2.0 England and Wales", "https://spdx.org/licenses/CC-BY-NC-SA-2.0-UK.html"), License("CC-BY-NC-SA-2.5", "Creative Commons Attribution Non Commercial Share Alike 2.5 Generic", "https://spdx.org/licenses/CC-BY-NC-SA-2.5.html"), License("CC-BY-NC-SA-3.0", "Creative Commons Attribution Non Commercial Share Alike 3.0 Unported", "https://spdx.org/licenses/CC-BY-NC-SA-3.0.html"), License("CC-BY-NC-SA-3.0-DE", "Creative Commons Attribution Non Commercial Share Alike 3.0 Germany", "https://spdx.org/licenses/CC-BY-NC-SA-3.0-DE.html"), License("CC-BY-NC-SA-3.0-IGO", "Creative Commons Attribution Non Commercial Share Alike 3.0 IGO", "https://spdx.org/licenses/CC-BY-NC-SA-3.0-IGO.html"), License("CC-BY-NC-SA-4.0", "Creative Commons Attribution Non Commercial Share Alike 4.0 International", "https://spdx.org/licenses/CC-BY-NC-SA-4.0.html"), License("CC-BY-ND-1.0", "Creative Commons Attribution No Derivatives 1.0 Generic", "https://spdx.org/licenses/CC-BY-ND-1.0.html"), License("CC-BY-ND-2.0", "Creative Commons Attribution No Derivatives 2.0 Generic", "https://spdx.org/licenses/CC-BY-ND-2.0.html"), License("CC-BY-ND-2.5", "Creative Commons Attribution No Derivatives 2.5 Generic", "https://spdx.org/licenses/CC-BY-ND-2.5.html"), License("CC-BY-ND-3.0", "Creative Commons Attribution No Derivatives 3.0 Unported", "https://spdx.org/licenses/CC-BY-ND-3.0.html"), License("CC-BY-ND-3.0-DE", "Creative Commons Attribution No Derivatives 3.0 Germany", "https://spdx.org/licenses/CC-BY-ND-3.0-DE.html"), License("CC-BY-ND-4.0", "Creative Commons Attribution No Derivatives 4.0 International", "https://spdx.org/licenses/CC-BY-ND-4.0.html"), License("CC-BY-SA-1.0", "Creative Commons Attribution Share Alike 1.0 Generic", "https://spdx.org/licenses/CC-BY-SA-1.0.html"), License("CC-BY-SA-2.0", "Creative Commons Attribution Share Alike 2.0 Generic", "https://spdx.org/licenses/CC-BY-SA-2.0.html"), License("CC-BY-SA-2.0-UK", "Creative Commons Attribution Share Alike 2.0 England and Wales", "https://spdx.org/licenses/CC-BY-SA-2.0-UK.html"), License("CC-BY-SA-2.1-JP", "Creative Commons Attribution Share Alike 2.1 Japan", "https://spdx.org/licenses/CC-BY-SA-2.1-JP.html"), License("CC-BY-SA-2.5", "Creative Commons Attribution Share Alike 2.5 Generic", "https://spdx.org/licenses/CC-BY-SA-2.5.html"), License("CC-BY-SA-3.0", "Creative Commons Attribution Share Alike 3.0 Unported", "https://spdx.org/licenses/CC-BY-SA-3.0.html"), License("CC-BY-SA-3.0-AT", "Creative Commons Attribution Share Alike 3.0 Austria", "https://spdx.org/licenses/CC-BY-SA-3.0-AT.html"), License("CC-BY-SA-3.0-DE", "Creative Commons Attribution Share Alike 3.0 Germany", "https://spdx.org/licenses/CC-BY-SA-3.0-DE.html"), License("CC-BY-SA-4.0", "Creative Commons Attribution Share Alike 4.0 International", "https://spdx.org/licenses/CC-BY-SA-4.0.html"), License("CC-PDDC", "Creative Commons Public Domain Dedication and Certification", "https://spdx.org/licenses/CC-PDDC.html"), License("CC0-1.0", "Creative Commons Zero v1.0 Universal", "https://spdx.org/licenses/CC0-1.0.html"), License("CDDL-1.0", "Common Development and Distribution License 1.0", "https://spdx.org/licenses/CDDL-1.0.html"), License("CDDL-1.1", "Common Development and Distribution License 1.1", "https://spdx.org/licenses/CDDL-1.1.html"), License("CDL-1.0", "Common Documentation License 1.0", "https://spdx.org/licenses/CDL-1.0.html"), License("CDLA-Permissive-1.0", "Community Data License Agreement Permissive 1.0", "https://spdx.org/licenses/CDLA-Permissive-1.0.html"), License("CDLA-Permissive-2.0", "Community Data License Agreement Permissive 2.0", "https://spdx.org/licenses/CDLA-Permissive-2.0.html"), License("CDLA-Sharing-1.0", "Community Data License Agreement Sharing 1.0", "https://spdx.org/licenses/CDLA-Sharing-1.0.html"), License("CECILL-1.0", "CeCILL Free Software License Agreement v1.0", "https://spdx.org/licenses/CECILL-1.0.html"), License("CECILL-1.1", "CeCILL Free Software License Agreement v1.1", "https://spdx.org/licenses/CECILL-1.1.html"), License("CECILL-2.0", "CeCILL Free Software License Agreement v2.0", "https://spdx.org/licenses/CECILL-2.0.html"), License("CECILL-2.1", "CeCILL Free Software License Agreement v2.1", "https://spdx.org/licenses/CECILL-2.1.html"), License("CECILL-B", "CeCILL-B Free Software License Agreement", "https://spdx.org/licenses/CECILL-B.html"), License("CECILL-C", "CeCILL-C Free Software License Agreement", "https://spdx.org/licenses/CECILL-C.html"), License("CERN-OHL-1.1", "CERN Open Hardware Licence v1.1", "https://spdx.org/licenses/CERN-OHL-1.1.html"), License("CERN-OHL-1.2", "CERN Open Hardware Licence v1.2", "https://spdx.org/licenses/CERN-OHL-1.2.html"), License("CERN-OHL-P-2.0", "CERN Open Hardware Licence Version 2 - Permissive", "https://spdx.org/licenses/CERN-OHL-P-2.0.html"), License("CERN-OHL-S-2.0", "CERN Open Hardware Licence Version 2 - Strongly Reciprocal", "https://spdx.org/licenses/CERN-OHL-S-2.0.html"), License("CERN-OHL-W-2.0", "CERN Open Hardware Licence Version 2 - Weakly Reciprocal", "https://spdx.org/licenses/CERN-OHL-W-2.0.html"), License("CNRI-Jython", "CNRI Jython License", "https://spdx.org/licenses/CNRI-Jython.html"), License("CNRI-Python", "CNRI Python License", "https://spdx.org/licenses/CNRI-Python.html"), License("CNRI-Python-GPL-Compatible", "CNRI Python Open Source GPL Compatible License Agreement", "https://spdx.org/licenses/CNRI-Python-GPL-Compatible.html"), License("COIL-1.0", "Copyfree Open Innovation License", "https://spdx.org/licenses/COIL-1.0.html"), License("CPAL-1.0", "Common Public Attribution License 1.0", "https://spdx.org/licenses/CPAL-1.0.html"), License("CPL-1.0", "Common Public License 1.0", "https://spdx.org/licenses/CPL-1.0.html"), License("CPOL-1.02", "Code Project Open License 1.02", "https://spdx.org/licenses/CPOL-1.02.html"), License("CUA-OPL-1.0", "CUA Office Public License v1.0", "https://spdx.org/licenses/CUA-OPL-1.0.html"), License("Caldera", "Caldera License", "https://spdx.org/licenses/Caldera.html"), License("ClArtistic", "Clarified Artistic License", "https://spdx.org/licenses/ClArtistic.html"), License("Community-Spec-1.0", "Community Specification License 1.0", "https://spdx.org/licenses/Community-Spec-1.0.html"), License("Condor-1.1", "Condor Public License v1.1", "https://spdx.org/licenses/Condor-1.1.html"), License("Crossword", "Crossword License", "https://spdx.org/licenses/Crossword.html"), License("CrystalStacker", "CrystalStacker License", "https://spdx.org/licenses/CrystalStacker.html"), License("Cube", "Cube License", "https://spdx.org/licenses/Cube.html"), License("D-FSL-1.0", "Deutsche Freie Software Lizenz", "https://spdx.org/licenses/D-FSL-1.0.html"), License("DL-DE-BY-2.0", "Data licence Germany – attribution – version 2.0", "https://spdx.org/licenses/DL-DE-BY-2.0.html"), License("DOC", "DOC License", "https://spdx.org/licenses/DOC.html"), License("DRL-1.0", "Detection Rule License 1.0", "https://spdx.org/licenses/DRL-1.0.html"), License("DSDP", "DSDP License", "https://spdx.org/licenses/DSDP.html"), License("Dotseqn", "Dotseqn License", "https://spdx.org/licenses/Dotseqn.html"), License("ECL-1.0", "Educational Community License v1.0", "https://spdx.org/licenses/ECL-1.0.html"), License("ECL-2.0", "Educational Community License v2.0", "https://spdx.org/licenses/ECL-2.0.html"), License("EFL-1.0", "Eiffel Forum License v1.0", "https://spdx.org/licenses/EFL-1.0.html"), License("EFL-2.0", "Eiffel Forum License v2.0", "https://spdx.org/licenses/EFL-2.0.html"), License("EPICS", "EPICS Open License", "https://spdx.org/licenses/EPICS.html"), License("EPL-1.0", "Eclipse Public License 1.0", "https://spdx.org/licenses/EPL-1.0.html"), License("EPL-2.0", "Eclipse Public License 2.0", "https://spdx.org/licenses/EPL-2.0.html"), License("EUDatagrid", "EU DataGrid Software License", "https://spdx.org/licenses/EUDatagrid.html"), License("EUPL-1.0", "European Union Public License 1.0", "https://spdx.org/licenses/EUPL-1.0.html"), License("EUPL-1.1", "European Union Public License 1.1", "https://spdx.org/licenses/EUPL-1.1.html"), License("EUPL-1.2", "European Union Public License 1.2", "https://spdx.org/licenses/EUPL-1.2.html"), License("Elastic-2.0", "Elastic License 2.0", "https://spdx.org/licenses/Elastic-2.0.html"), License("Entessa", "Entessa Public License v1.0", "https://spdx.org/licenses/Entessa.html"), License("ErlPL-1.1", "Erlang Public License v1.1", "https://spdx.org/licenses/ErlPL-1.1.html"), License("Eurosym", "Eurosym License", "https://spdx.org/licenses/Eurosym.html"), License("FDK-AAC", "Fraunhofer FDK AAC Codec Library", "https://spdx.org/licenses/FDK-AAC.html"), License("FSFAP", "FSF All Permissive License", "https://spdx.org/licenses/FSFAP.html"), License("FSFUL", "FSF Unlimited License", "https://spdx.org/licenses/FSFUL.html"), License("FSFULLR", "FSF Unlimited License (with License Retention)", "https://spdx.org/licenses/FSFULLR.html"), License("FTL", "Freetype Project License", "https://spdx.org/licenses/FTL.html"), License("Fair", "Fair License", "https://spdx.org/licenses/Fair.html"), License("Frameworx-1.0", "Frameworx Open License 1.0", "https://spdx.org/licenses/Frameworx-1.0.html"), License("FreeBSD-DOC", "FreeBSD Documentation License", "https://spdx.org/licenses/FreeBSD-DOC.html"), License("FreeImage", "FreeImage Public License v1.0", "https://spdx.org/licenses/FreeImage.html"), License("GD", "GD License", "https://spdx.org/licenses/GD.html"), License("GFDL-1.1", "GNU Free Documentation License v1.1", "https://spdx.org/licenses/GFDL-1.1.html"), License("GFDL-1.1-invariants-only", "GNU Free Documentation License v1.1 only - invariants", "https://spdx.org/licenses/GFDL-1.1-invariants-only.html"), License("GFDL-1.1-invariants-or-later", "GNU Free Documentation License v1.1 or later - invariants", "https://spdx.org/licenses/GFDL-1.1-invariants-or-later.html"), License("GFDL-1.1-no-invariants-only", "GNU Free Documentation License v1.1 only - no invariants", "https://spdx.org/licenses/GFDL-1.1-no-invariants-only.html"), License("GFDL-1.1-no-invariants-or-later", "GNU Free Documentation License v1.1 or later - no invariants", "https://spdx.org/licenses/GFDL-1.1-no-invariants-or-later.html"), License("GFDL-1.1-only", "GNU Free Documentation License v1.1 only", "https://spdx.org/licenses/GFDL-1.1-only.html"), License("GFDL-1.1-or-later", "GNU Free Documentation License v1.1 or later", "https://spdx.org/licenses/GFDL-1.1-or-later.html"), License("GFDL-1.2", "GNU Free Documentation License v1.2", "https://spdx.org/licenses/GFDL-1.2.html"), License("GFDL-1.2-invariants-only", "GNU Free Documentation License v1.2 only - invariants", "https://spdx.org/licenses/GFDL-1.2-invariants-only.html"), License("GFDL-1.2-invariants-or-later", "GNU Free Documentation License v1.2 or later - invariants", "https://spdx.org/licenses/GFDL-1.2-invariants-or-later.html"), License("GFDL-1.2-no-invariants-only", "GNU Free Documentation License v1.2 only - no invariants", "https://spdx.org/licenses/GFDL-1.2-no-invariants-only.html"), License("GFDL-1.2-no-invariants-or-later", "GNU Free Documentation License v1.2 or later - no invariants", "https://spdx.org/licenses/GFDL-1.2-no-invariants-or-later.html"), License("GFDL-1.2-only", "GNU Free Documentation License v1.2 only", "https://spdx.org/licenses/GFDL-1.2-only.html"), License("GFDL-1.2-or-later", "GNU Free Documentation License v1.2 or later", "https://spdx.org/licenses/GFDL-1.2-or-later.html"), License("GFDL-1.3", "GNU Free Documentation License v1.3", "https://spdx.org/licenses/GFDL-1.3.html"), License("GFDL-1.3-invariants-only", "GNU Free Documentation License v1.3 only - invariants", "https://spdx.org/licenses/GFDL-1.3-invariants-only.html"), License("GFDL-1.3-invariants-or-later", "GNU Free Documentation License v1.3 or later - invariants", "https://spdx.org/licenses/GFDL-1.3-invariants-or-later.html"), License("GFDL-1.3-no-invariants-only", "GNU Free Documentation License v1.3 only - no invariants", "https://spdx.org/licenses/GFDL-1.3-no-invariants-only.html"), License("GFDL-1.3-no-invariants-or-later", "GNU Free Documentation License v1.3 or later - no invariants", "https://spdx.org/licenses/GFDL-1.3-no-invariants-or-later.html"), License("GFDL-1.3-only", "GNU Free Documentation License v1.3 only", "https://spdx.org/licenses/GFDL-1.3-only.html"), License("GFDL-1.3-or-later", "GNU Free Documentation License v1.3 or later", "https://spdx.org/licenses/GFDL-1.3-or-later.html"), License("GL2PS", "GL2PS License", "https://spdx.org/licenses/GL2PS.html"), License("GLWTPL", "Good Luck With That Public License", "https://spdx.org/licenses/GLWTPL.html"), License("GPL-1.0", "GNU General Public License v1.0 only", "https://spdx.org/licenses/GPL-1.0.html"), License("GPL-1.0+", "GNU General Public License v1.0 or later", "https://spdx.org/licenses/GPL-1.0+.html"), License("GPL-1.0-only", "GNU General Public License v1.0 only", "https://spdx.org/licenses/GPL-1.0-only.html"), License("GPL-1.0-or-later", "GNU General Public License v1.0 or later", "https://spdx.org/licenses/GPL-1.0-or-later.html"), License("GPL-2.0", "GNU General Public License v2.0 only", "https://spdx.org/licenses/GPL-2.0.html"), License("GPL-2.0+", "GNU General Public License v2.0 or later", "https://spdx.org/licenses/GPL-2.0+.html"), License("GPL-2.0-only", "GNU General Public License v2.0 only", "https://spdx.org/licenses/GPL-2.0-only.html"), License("GPL-2.0-or-later", "GNU General Public License v2.0 or later", "https://spdx.org/licenses/GPL-2.0-or-later.html"), License("GPL-2.0-with-GCC-exception", "GNU General Public License v2.0 w/GCC Runtime Library exception", "https://spdx.org/licenses/GPL-2.0-with-GCC-exception.html"), License("GPL-2.0-with-autoconf-exception", "GNU General Public License v2.0 w/Autoconf exception", "https://spdx.org/licenses/GPL-2.0-with-autoconf-exception.html"), License("GPL-2.0-with-bison-exception", "GNU General Public License v2.0 w/Bison exception", "https://spdx.org/licenses/GPL-2.0-with-bison-exception.html"), License("GPL-2.0-with-classpath-exception", "GNU General Public License v2.0 w/Classpath exception", "https://spdx.org/licenses/GPL-2.0-with-classpath-exception.html"), License("GPL-2.0-with-font-exception", "GNU General Public License v2.0 w/Font exception", "https://spdx.org/licenses/GPL-2.0-with-font-exception.html"), License("GPL-3.0", "GNU General Public License v3.0 only", "https://spdx.org/licenses/GPL-3.0.html"), License("GPL-3.0+", "GNU General Public License v3.0 or later", "https://spdx.org/licenses/GPL-3.0+.html"), License("GPL-3.0-only", "GNU General Public License v3.0 only", "https://spdx.org/licenses/GPL-3.0-only.html"), License("GPL-3.0-or-later", "GNU General Public License v3.0 or later", "https://spdx.org/licenses/GPL-3.0-or-later.html"), License("GPL-3.0-with-GCC-exception", "GNU General Public License v3.0 w/GCC Runtime Library exception", "https://spdx.org/licenses/GPL-3.0-with-GCC-exception.html"), License("GPL-3.0-with-autoconf-exception", "GNU General Public License v3.0 w/Autoconf exception", "https://spdx.org/licenses/GPL-3.0-with-autoconf-exception.html"), License("Giftware", "Giftware License", "https://spdx.org/licenses/Giftware.html"), License("Glide", "3dfx Glide License", "https://spdx.org/licenses/Glide.html"), License("Glulxe", "Glulxe License", "https://spdx.org/licenses/Glulxe.html"), License("HPND", "Historical Permission Notice and Disclaimer", "https://spdx.org/licenses/HPND.html"), License("HPND-sell-variant", "Historical Permission Notice and Disclaimer - sell variant", "https://spdx.org/licenses/HPND-sell-variant.html"), License("HTMLTIDY", "HTML Tidy License", "https://spdx.org/licenses/HTMLTIDY.html"), License("HaskellReport", "Haskell Language Report License", "https://spdx.org/licenses/HaskellReport.html"), License("Hippocratic-2.1", "Hippocratic License 2.1", "https://spdx.org/licenses/Hippocratic-2.1.html"), License("IBM-pibs", "IBM PowerPC Initialization and Boot Software", "https://spdx.org/licenses/IBM-pibs.html"), License("ICU", "ICU License", "https://spdx.org/licenses/ICU.html"), License("IJG", "Independent JPEG Group License", "https://spdx.org/licenses/IJG.html"), License("IPA", "IPA Font License", "https://spdx.org/licenses/IPA.html"), License("IPL-1.0", "IBM Public License v1.0", "https://spdx.org/licenses/IPL-1.0.html"), License("ISC", "ISC License", "https://spdx.org/licenses/ISC.html"), License("ImageMagick", "ImageMagick License", "https://spdx.org/licenses/ImageMagick.html"), License("Imlib2", "Imlib2 License", "https://spdx.org/licenses/Imlib2.html"), License("Info-ZIP", "Info-ZIP License", "https://spdx.org/licenses/Info-ZIP.html"), License("Intel", "Intel Open Source License", "https://spdx.org/licenses/Intel.html"), License("Intel-ACPI", "Intel ACPI Software License Agreement", "https://spdx.org/licenses/Intel-ACPI.html"), License("Interbase-1.0", "Interbase Public License v1.0", "https://spdx.org/licenses/Interbase-1.0.html"), License("JPNIC", "Japan Network Information Center License", "https://spdx.org/licenses/JPNIC.html"), License("JSON", "JSON License", "https://spdx.org/licenses/JSON.html"), License("Jam", "Jam License", "https://spdx.org/licenses/Jam.html"), License("JasPer-2.0", "JasPer License", "https://spdx.org/licenses/JasPer-2.0.html"), License("LAL-1.2", "Licence Art Libre 1.2", "https://spdx.org/licenses/LAL-1.2.html"), License("LAL-1.3", "Licence Art Libre 1.3", "https://spdx.org/licenses/LAL-1.3.html"), License("LGPL-2.0", "GNU Library General Public License v2 only", "https://spdx.org/licenses/LGPL-2.0.html"), License("LGPL-2.0+", "GNU Library General Public License v2 or later", "https://spdx.org/licenses/LGPL-2.0+.html"), License("LGPL-2.0-only", "GNU Library General Public License v2 only", "https://spdx.org/licenses/LGPL-2.0-only.html"), License("LGPL-2.0-or-later", "GNU Library General Public License v2 or later", "https://spdx.org/licenses/LGPL-2.0-or-later.html"), License("LGPL-2.1", "GNU Lesser General Public License v2.1 only", "https://spdx.org/licenses/LGPL-2.1.html"), License("LGPL-2.1+", "GNU Library General Public License v2.1 or later", "https://spdx.org/licenses/LGPL-2.1+.html"), License("LGPL-2.1-only", "GNU Lesser General Public License v2.1 only", "https://spdx.org/licenses/LGPL-2.1-only.html"), License("LGPL-2.1-or-later", "GNU Lesser General Public License v2.1 or later", "https://spdx.org/licenses/LGPL-2.1-or-later.html"), License("LGPL-3.0", "GNU Lesser General Public License v3.0 only", "https://spdx.org/licenses/LGPL-3.0.html"), License("LGPL-3.0+", "GNU Lesser General Public License v3.0 or later", "https://spdx.org/licenses/LGPL-3.0+.html"), License("LGPL-3.0-only", "GNU Lesser General Public License v3.0 only", "https://spdx.org/licenses/LGPL-3.0-only.html"), License("LGPL-3.0-or-later", "GNU Lesser General Public License v3.0 or later", "https://spdx.org/licenses/LGPL-3.0-or-later.html"), License("LGPLLR", "Lesser General Public License For Linguistic Resources", "https://spdx.org/licenses/LGPLLR.html"), License("LPL-1.0", "Lucent Public License Version 1.0", "https://spdx.org/licenses/LPL-1.0.html"), License("LPL-1.02", "Lucent Public License v1.02", "https://spdx.org/licenses/LPL-1.02.html"), License("LPPL-1.0", "LaTeX Project Public License v1.0", "https://spdx.org/licenses/LPPL-1.0.html"), License("LPPL-1.1", "LaTeX Project Public License v1.1", "https://spdx.org/licenses/LPPL-1.1.html"), License("LPPL-1.2", "LaTeX Project Public License v1.2", "https://spdx.org/licenses/LPPL-1.2.html"), License("LPPL-1.3a", "LaTeX Project Public License v1.3a", "https://spdx.org/licenses/LPPL-1.3a.html"), License("LPPL-1.3c", "LaTeX Project Public License v1.3c", "https://spdx.org/licenses/LPPL-1.3c.html"), License("Latex2e", "Latex2e License", "https://spdx.org/licenses/Latex2e.html"), License("Leptonica", "Leptonica License", "https://spdx.org/licenses/Leptonica.html"), License("LiLiQ-P-1.1", "Licence Libre du Québec – Permissive version 1.1", "https://spdx.org/licenses/LiLiQ-P-1.1.html"), License("LiLiQ-R-1.1", "Licence Libre du Québec – Réciprocité version 1.1", "https://spdx.org/licenses/LiLiQ-R-1.1.html"), License("LiLiQ-Rplus-1.1", "Licence Libre du Québec – Réciprocité forte version 1.1", "https://spdx.org/licenses/LiLiQ-Rplus-1.1.html"), License("Libpng", "libpng License", "https://spdx.org/licenses/Libpng.html"), License("Linux-OpenIB", "Linux Kernel Variant of OpenIB.org license", "https://spdx.org/licenses/Linux-OpenIB.html"), License("Linux-man-pages-copyleft", "Linux man-pages Copyleft", "https://spdx.org/licenses/Linux-man-pages-copyleft.html"), License("MIT", "MIT License", "https://spdx.org/licenses/MIT.html"), License("MIT-0", "MIT No Attribution", "https://spdx.org/licenses/MIT-0.html"), License("MIT-CMU", "CMU License", "https://spdx.org/licenses/MIT-CMU.html"), License("MIT-Modern-Variant", "MIT License Modern Variant", "https://spdx.org/licenses/MIT-Modern-Variant.html"), License("MIT-advertising", "Enlightenment License (e16)", "https://spdx.org/licenses/MIT-advertising.html"), License("MIT-enna", "enna License", "https://spdx.org/licenses/MIT-enna.html"), License("MIT-feh", "feh License", "https://spdx.org/licenses/MIT-feh.html"), License("MIT-open-group", "MIT Open Group variant", "https://spdx.org/licenses/MIT-open-group.html"), License("MITNFA", "MIT +no-false-attribs license", "https://spdx.org/licenses/MITNFA.html"), License("MPL-1.0", "Mozilla Public License 1.0", "https://spdx.org/licenses/MPL-1.0.html"), License("MPL-1.1", "Mozilla Public License 1.1", "https://spdx.org/licenses/MPL-1.1.html"), License("MPL-2.0", "Mozilla Public License 2.0", "https://spdx.org/licenses/MPL-2.0.html"), License("MPL-2.0-no-copyleft-exception", "Mozilla Public License 2.0 (no copyleft exception)", "https://spdx.org/licenses/MPL-2.0-no-copyleft-exception.html"), License("MS-PL", "Microsoft Public License", "https://spdx.org/licenses/MS-PL.html"), License("MS-RL", "Microsoft Reciprocal License", "https://spdx.org/licenses/MS-RL.html"), License("MTLL", "Matrix Template Library License", "https://spdx.org/licenses/MTLL.html"), License("MakeIndex", "MakeIndex License", "https://spdx.org/licenses/MakeIndex.html"), License("MirOS", "The MirOS Licence", "https://spdx.org/licenses/MirOS.html"), License("Motosoto", "Motosoto License", "https://spdx.org/licenses/Motosoto.html"), License("MulanPSL-1.0", "Mulan Permissive Software License, Version 1", "https://spdx.org/licenses/MulanPSL-1.0.html"), License("MulanPSL-2.0", "Mulan Permissive Software License, Version 2", "https://spdx.org/licenses/MulanPSL-2.0.html"), License("Multics", "Multics License", "https://spdx.org/licenses/Multics.html"), License("Mup", "Mup License", "https://spdx.org/licenses/Mup.html"), License("NAIST-2003", "Nara Institute of Science and Technology License (2003)", "https://spdx.org/licenses/NAIST-2003.html"), License("NASA-1.3", "NASA Open Source Agreement 1.3", "https://spdx.org/licenses/NASA-1.3.html"), License("NBPL-1.0", "Net Boolean Public License v1", "https://spdx.org/licenses/NBPL-1.0.html"), License("NCGL-UK-2.0", "Non-Commercial Government Licence", "https://spdx.org/licenses/NCGL-UK-2.0.html"), License("NCSA", "University of Illinois/NCSA Open Source License", "https://spdx.org/licenses/NCSA.html"), License("NGPL", "Nethack General Public License", "https://spdx.org/licenses/NGPL.html"), License("NIST-PD", "NIST Public Domain Notice", "https://spdx.org/licenses/NIST-PD.html"), License("NIST-PD-fallback", "NIST Public Domain Notice with license fallback", "https://spdx.org/licenses/NIST-PD-fallback.html"), License("NLOD-1.0", "Norwegian Licence for Open Government Data (NLOD) 1.0", "https://spdx.org/licenses/NLOD-1.0.html"), License("NLOD-2.0", "Norwegian Licence for Open Government Data (NLOD) 2.0", "https://spdx.org/licenses/NLOD-2.0.html"), License("NLPL", "No Limit Public License", "https://spdx.org/licenses/NLPL.html"), License("NOSL", "Netizen Open Source License", "https://spdx.org/licenses/NOSL.html"), License("NPL-1.0", "Netscape Public License v1.0", "https://spdx.org/licenses/NPL-1.0.html"), License("NPL-1.1", "Netscape Public License v1.1", "https://spdx.org/licenses/NPL-1.1.html"), License("NPOSL-3.0", "Non-Profit Open Software License 3.0", "https://spdx.org/licenses/NPOSL-3.0.html"), License("NRL", "NRL License", "https://spdx.org/licenses/NRL.html"), License("NTP", "NTP License", "https://spdx.org/licenses/NTP.html"), License("NTP-0", "NTP No Attribution", "https://spdx.org/licenses/NTP-0.html"), License("Naumen", "Naumen Public License", "https://spdx.org/licenses/Naumen.html"), License("Net-SNMP", "Net-SNMP License", "https://spdx.org/licenses/Net-SNMP.html"), License("NetCDF", "NetCDF license", "https://spdx.org/licenses/NetCDF.html"), License("Newsletr", "Newsletr License", "https://spdx.org/licenses/Newsletr.html"), License("Nokia", "Nokia Open Source License", "https://spdx.org/licenses/Nokia.html"), License("Noweb", "Noweb License", "https://spdx.org/licenses/Noweb.html"), License("Nunit", "Nunit License", "https://spdx.org/licenses/Nunit.html"), License("O-UDA-1.0", "Open Use of Data Agreement v1.0", "https://spdx.org/licenses/O-UDA-1.0.html"), License("OCCT-PL", "Open CASCADE Technology Public License", "https://spdx.org/licenses/OCCT-PL.html"), License("OCLC-2.0", "OCLC Research Public License 2.0", "https://spdx.org/licenses/OCLC-2.0.html"), License("ODC-By-1.0", "Open Data Commons Attribution License v1.0", "https://spdx.org/licenses/ODC-By-1.0.html"), License("ODbL-1.0", "Open Data Commons Open Database License v1.0", "https://spdx.org/licenses/ODbL-1.0.html"), License("OFL-1.0", "SIL Open Font License 1.0", "https://spdx.org/licenses/OFL-1.0.html"), License("OFL-1.0-RFN", "SIL Open Font License 1.0 with Reserved Font Name", "https://spdx.org/licenses/OFL-1.0-RFN.html"), License("OFL-1.0-no-RFN", "SIL Open Font License 1.0 with no Reserved Font Name", "https://spdx.org/licenses/OFL-1.0-no-RFN.html"), License("OFL-1.1", "SIL Open Font License 1.1", "https://spdx.org/licenses/OFL-1.1.html"), License("OFL-1.1-RFN", "SIL Open Font License 1.1 with Reserved Font Name", "https://spdx.org/licenses/OFL-1.1-RFN.html"), License("OFL-1.1-no-RFN", "SIL Open Font License 1.1 with no Reserved Font Name", "https://spdx.org/licenses/OFL-1.1-no-RFN.html"), License("OGC-1.0", "OGC Software License, Version 1.0", "https://spdx.org/licenses/OGC-1.0.html"), License("OGDL-Taiwan-1.0", "Taiwan Open Government Data License, version 1.0", "https://spdx.org/licenses/OGDL-Taiwan-1.0.html"), License("OGL-Canada-2.0", "Open Government Licence - Canada", "https://spdx.org/licenses/OGL-Canada-2.0.html"), License("OGL-UK-1.0", "Open Government Licence v1.0", "https://spdx.org/licenses/OGL-UK-1.0.html"), License("OGL-UK-2.0", "Open Government Licence v2.0", "https://spdx.org/licenses/OGL-UK-2.0.html"), License("OGL-UK-3.0", "Open Government Licence v3.0", "https://spdx.org/licenses/OGL-UK-3.0.html"), License("OGTSL", "Open Group Test Suite License", "https://spdx.org/licenses/OGTSL.html"), License("OLDAP-1.1", "Open LDAP Public License v1.1", "https://spdx.org/licenses/OLDAP-1.1.html"), License("OLDAP-1.2", "Open LDAP Public License v1.2", "https://spdx.org/licenses/OLDAP-1.2.html"), License("OLDAP-1.3", "Open LDAP Public License v1.3", "https://spdx.org/licenses/OLDAP-1.3.html"), License("OLDAP-1.4", "Open LDAP Public License v1.4", "https://spdx.org/licenses/OLDAP-1.4.html"), License("OLDAP-2.0", "Open LDAP Public License v2.0 (or possibly 2.0A and 2.0B)", "https://spdx.org/licenses/OLDAP-2.0.html"), License("OLDAP-2.0.1", "Open LDAP Public License v2.0.1", "https://spdx.org/licenses/OLDAP-2.0.1.html"), License("OLDAP-2.1", "Open LDAP Public License v2.1", "https://spdx.org/licenses/OLDAP-2.1.html"), License("OLDAP-2.2", "Open LDAP Public License v2.2", "https://spdx.org/licenses/OLDAP-2.2.html"), License("OLDAP-2.2.1", "Open LDAP Public License v2.2.1", "https://spdx.org/licenses/OLDAP-2.2.1.html"), License("OLDAP-2.2.2", "Open LDAP Public License 2.2.2", "https://spdx.org/licenses/OLDAP-2.2.2.html"), License("OLDAP-2.3", "Open LDAP Public License v2.3", "https://spdx.org/licenses/OLDAP-2.3.html"), License("OLDAP-2.4", "Open LDAP Public License v2.4", "https://spdx.org/licenses/OLDAP-2.4.html"), License("OLDAP-2.5", "Open LDAP Public License v2.5", "https://spdx.org/licenses/OLDAP-2.5.html"), License("OLDAP-2.6", "Open LDAP Public License v2.6", "https://spdx.org/licenses/OLDAP-2.6.html"), License("OLDAP-2.7", "Open LDAP Public License v2.7", "https://spdx.org/licenses/OLDAP-2.7.html"), License("OLDAP-2.8", "Open LDAP Public License v2.8", "https://spdx.org/licenses/OLDAP-2.8.html"), License("OML", "Open Market License", "https://spdx.org/licenses/OML.html"), License("OPL-1.0", "Open Public License v1.0", "https://spdx.org/licenses/OPL-1.0.html"), License("OPUBL-1.0", "Open Publication License v1.0", "https://spdx.org/licenses/OPUBL-1.0.html"), License("OSET-PL-2.1", "OSET Public License version 2.1", "https://spdx.org/licenses/OSET-PL-2.1.html"), License("OSL-1.0", "Open Software License 1.0", "https://spdx.org/licenses/OSL-1.0.html"), License("OSL-1.1", "Open Software License 1.1", "https://spdx.org/licenses/OSL-1.1.html"), License("OSL-2.0", "Open Software License 2.0", "https://spdx.org/licenses/OSL-2.0.html"), License("OSL-2.1", "Open Software License 2.1", "https://spdx.org/licenses/OSL-2.1.html"), License("OSL-3.0", "Open Software License 3.0", "https://spdx.org/licenses/OSL-3.0.html"), License("OpenSSL", "OpenSSL License", "https://spdx.org/licenses/OpenSSL.html"), License("PDDL-1.0", "Open Data Commons Public Domain Dedication & License 1.0", "https://spdx.org/licenses/PDDL-1.0.html"), License("PHP-3.0", "PHP License v3.0", "https://spdx.org/licenses/PHP-3.0.html"), License("PHP-3.01", "PHP License v3.01", "https://spdx.org/licenses/PHP-3.01.html"), License("PSF-2.0", "Python Software Foundation License 2.0", "https://spdx.org/licenses/PSF-2.0.html"), License("Parity-6.0.0", "The Parity Public License 6.0.0", "https://spdx.org/licenses/Parity-6.0.0.html"), License("Parity-7.0.0", "The Parity Public License 7.0.0", "https://spdx.org/licenses/Parity-7.0.0.html"), License("Plexus", "Plexus Classworlds License", "https://spdx.org/licenses/Plexus.html"), License("PolyForm-Noncommercial-1.0.0", "PolyForm Noncommercial License 1.0.0", "https://spdx.org/licenses/PolyForm-Noncommercial-1.0.0.html"), License("PolyForm-Small-Business-1.0.0", "PolyForm Small Business License 1.0.0", "https://spdx.org/licenses/PolyForm-Small-Business-1.0.0.html"), License("PostgreSQL", "PostgreSQL License", "https://spdx.org/licenses/PostgreSQL.html"), License("Python-2.0", "Python License 2.0", "https://spdx.org/licenses/Python-2.0.html"), License("QPL-1.0", "Q Public License 1.0", "https://spdx.org/licenses/QPL-1.0.html"), License("Qhull", "Qhull License", "https://spdx.org/licenses/Qhull.html"), License("RHeCos-1.1", "Red Hat eCos Public License v1.1", "https://spdx.org/licenses/RHeCos-1.1.html"), License("RPL-1.1", "Reciprocal Public License 1.1", "https://spdx.org/licenses/RPL-1.1.html"), License("RPL-1.5", "Reciprocal Public License 1.5", "https://spdx.org/licenses/RPL-1.5.html"), License("RPSL-1.0", "RealNetworks Public Source License v1.0", "https://spdx.org/licenses/RPSL-1.0.html"), License("RSA-MD", "RSA Message-Digest License", "https://spdx.org/licenses/RSA-MD.html"), License("RSCPL", "Ricoh Source Code Public License", "https://spdx.org/licenses/RSCPL.html"), License("Rdisc", "Rdisc License", "https://spdx.org/licenses/Rdisc.html"), License("Ruby", "Ruby License", "https://spdx.org/licenses/Ruby.html"), License("SAX-PD", "Sax Public Domain Notice", "https://spdx.org/licenses/SAX-PD.html"), License("SCEA", "SCEA Shared Source License", "https://spdx.org/licenses/SCEA.html"), License("SGI-B-1.0", "SGI Free Software License B v1.0", "https://spdx.org/licenses/SGI-B-1.0.html"), License("SGI-B-1.1", "SGI Free Software License B v1.1", "https://spdx.org/licenses/SGI-B-1.1.html"), License("SGI-B-2.0", "SGI Free Software License B v2.0", "https://spdx.org/licenses/SGI-B-2.0.html"), License("SHL-0.5", "Solderpad Hardware License v0.5", "https://spdx.org/licenses/SHL-0.5.html"), License("SHL-0.51", "Solderpad Hardware License, Version 0.51", "https://spdx.org/licenses/SHL-0.51.html"), License("SISSL", "Sun Industry Standards Source License v1.1", "https://spdx.org/licenses/SISSL.html"), License("SISSL-1.2", "Sun Industry Standards Source License v1.2", "https://spdx.org/licenses/SISSL-1.2.html"), License("SMLNJ", "Standard ML of New Jersey License", "https://spdx.org/licenses/SMLNJ.html"), License("SMPPL", "Secure Messaging Protocol Public License", "https://spdx.org/licenses/SMPPL.html"), License("SNIA", "SNIA Public License 1.1", "https://spdx.org/licenses/SNIA.html"), License("SPL-1.0", "Sun Public License v1.0", "https://spdx.org/licenses/SPL-1.0.html"), License("SSH-OpenSSH", "SSH OpenSSH license", "https://spdx.org/licenses/SSH-OpenSSH.html"), License("SSH-short", "SSH short notice", "https://spdx.org/licenses/SSH-short.html"), License("SSPL-1.0", "Server Side Public License, v 1", "https://spdx.org/licenses/SSPL-1.0.html"), License("SWL", "Scheme Widget Library (SWL) Software License Agreement", "https://spdx.org/licenses/SWL.html"), License("Saxpath", "Saxpath License", "https://spdx.org/licenses/Saxpath.html"), License("SchemeReport", "Scheme Language Report License", "https://spdx.org/licenses/SchemeReport.html"), License("Sendmail", "Sendmail License", "https://spdx.org/licenses/Sendmail.html"), License("Sendmail-8.23", "Sendmail License 8.23", "https://spdx.org/licenses/Sendmail-8.23.html"), License("SimPL-2.0", "Simple Public License 2.0", "https://spdx.org/licenses/SimPL-2.0.html"), License("Sleepycat", "Sleepycat License", "https://spdx.org/licenses/Sleepycat.html"), License("Spencer-86", "Spencer License 86", "https://spdx.org/licenses/Spencer-86.html"), License("Spencer-94", "Spencer License 94", "https://spdx.org/licenses/Spencer-94.html"), License("Spencer-99", "Spencer License 99", "https://spdx.org/licenses/Spencer-99.html"), License("StandardML-NJ", "Standard ML of New Jersey License", "https://spdx.org/licenses/StandardML-NJ.html"), License("SugarCRM-1.1.3", "SugarCRM Public License v1.1.3", "https://spdx.org/licenses/SugarCRM-1.1.3.html"), License("TAPR-OHL-1.0", "TAPR Open Hardware License v1.0", "https://spdx.org/licenses/TAPR-OHL-1.0.html"), License("TCL", "TCL/TK License", "https://spdx.org/licenses/TCL.html"), License("TCP-wrappers", "TCP Wrappers License", "https://spdx.org/licenses/TCP-wrappers.html"), License("TMate", "TMate Open Source License", "https://spdx.org/licenses/TMate.html"), License("TORQUE-1.1", "TORQUE v2.5+ Software License v1.1", "https://spdx.org/licenses/TORQUE-1.1.html"), License("TOSL", "Trusster Open Source License", "https://spdx.org/licenses/TOSL.html"), License("TU-Berlin-1.0", "Technische Universitaet Berlin License 1.0", "https://spdx.org/licenses/TU-Berlin-1.0.html"), License("TU-Berlin-2.0", "Technische Universitaet Berlin License 2.0", "https://spdx.org/licenses/TU-Berlin-2.0.html"), License("UCL-1.0", "Upstream Compatibility License v1.0", "https://spdx.org/licenses/UCL-1.0.html"), License("UPL-1.0", "Universal Permissive License v1.0", "https://spdx.org/licenses/UPL-1.0.html"), License("Unicode-DFS-2015", "Unicode License Agreement - Data Files and Software (2015)", "https://spdx.org/licenses/Unicode-DFS-2015.html"), License("Unicode-DFS-2016", "Unicode License Agreement - Data Files and Software (2016)", "https://spdx.org/licenses/Unicode-DFS-2016.html"), License("Unicode-TOU", "Unicode Terms of Use", "https://spdx.org/licenses/Unicode-TOU.html"), License("Unlicense", "The Unlicense", "https://spdx.org/licenses/Unlicense.html"), License("VOSTROM", "VOSTROM Public License for Open Source", "https://spdx.org/licenses/VOSTROM.html"), License("VSL-1.0", "Vovida Software License v1.0", "https://spdx.org/licenses/VSL-1.0.html"), License("Vim", "Vim License", "https://spdx.org/licenses/Vim.html"), License("W3C", "W3C Software Notice and License (2002-12-31)", "https://spdx.org/licenses/W3C.html"), License("W3C-19980720", "W3C Software Notice and License (1998-07-20)", "https://spdx.org/licenses/W3C-19980720.html"), License("W3C-20150513", "W3C Software Notice and Document License (2015-05-13)", "https://spdx.org/licenses/W3C-20150513.html"), License("WTFPL", "Do What The F*ck You Want To Public License", "https://spdx.org/licenses/WTFPL.html"), License("Watcom-1.0", "Sybase Open Watcom Public License 1.0", "https://spdx.org/licenses/Watcom-1.0.html"), License("Wsuipa", "Wsuipa License", "https://spdx.org/licenses/Wsuipa.html"), License("X11", "X11 License", "https://spdx.org/licenses/X11.html"), License("X11-distribute-modifications-variant", "X11 License Distribution Modification Variant", "https://spdx.org/licenses/X11-distribute-modifications-variant.html"), License("XFree86-1.1", "XFree86 License 1.1", "https://spdx.org/licenses/XFree86-1.1.html"), License("XSkat", "XSkat License", "https://spdx.org/licenses/XSkat.html"), License("Xerox", "Xerox License", "https://spdx.org/licenses/Xerox.html"), License("Xnet", "X.Net License", "https://spdx.org/licenses/Xnet.html"), License("YPL-1.0", "Yahoo! Public License v1.0", "https://spdx.org/licenses/YPL-1.0.html"), License("YPL-1.1", "Yahoo! Public License v1.1", "https://spdx.org/licenses/YPL-1.1.html"), License("ZPL-1.1", "Zope Public License 1.1", "https://spdx.org/licenses/ZPL-1.1.html"), License("ZPL-2.0", "Zope Public License 2.0", "https://spdx.org/licenses/ZPL-2.0.html"), License("ZPL-2.1", "Zope Public License 2.1", "https://spdx.org/licenses/ZPL-2.1.html"), License("Zed", "Zed License", "https://spdx.org/licenses/Zed.html"), License("Zend-2.0", "Zend License v2.0", "https://spdx.org/licenses/Zend-2.0.html"), License("Zimbra-1.3", "Zimbra Public License v1.3", "https://spdx.org/licenses/Zimbra-1.3.html"), License("Zimbra-1.4", "Zimbra Public License v1.4", "https://spdx.org/licenses/Zimbra-1.4.html"), License("Zlib", "zlib License", "https://spdx.org/licenses/Zlib.html"), License("blessing", "SQLite Blessing", "https://spdx.org/licenses/blessing.html"), License("bzip2-1.0.5", "bzip2 and libbzip2 License v1.0.5", "https://spdx.org/licenses/bzip2-1.0.5.html"), License("bzip2-1.0.6", "bzip2 and libbzip2 License v1.0.6", "https://spdx.org/licenses/bzip2-1.0.6.html"), License("copyleft-next-0.3.0", "copyleft-next 0.3.0", "https://spdx.org/licenses/copyleft-next-0.3.0.html"), License("copyleft-next-0.3.1", "copyleft-next 0.3.1", "https://spdx.org/licenses/copyleft-next-0.3.1.html"), License("curl", "curl License", "https://spdx.org/licenses/curl.html"), License("diffmark", "diffmark license", "https://spdx.org/licenses/diffmark.html"), License("dvipdfm", "dvipdfm License", "https://spdx.org/licenses/dvipdfm.html"), License("eCos-2.0", "eCos license version 2.0", "https://spdx.org/licenses/eCos-2.0.html"), License("eGenix", "eGenix.com Public License 1.1.0", "https://spdx.org/licenses/eGenix.html"), License("etalab-2.0", "Etalab Open License 2.0", "https://spdx.org/licenses/etalab-2.0.html"), License("gSOAP-1.3b", "gSOAP Public License v1.3b", "https://spdx.org/licenses/gSOAP-1.3b.html"), License("gnuplot", "gnuplot License", "https://spdx.org/licenses/gnuplot.html"), License("iMatix", "iMatix Standard Function Library Agreement", "https://spdx.org/licenses/iMatix.html"), License("libpng-2.0", "PNG Reference Library version 2", "https://spdx.org/licenses/libpng-2.0.html"), License("libselinux-1.0", "libselinux public domain notice", "https://spdx.org/licenses/libselinux-1.0.html"), License("libtiff", "libtiff License", "https://spdx.org/licenses/libtiff.html"), License("mpich2", "mpich2 License", "https://spdx.org/licenses/mpich2.html"), License("psfrag", "psfrag License", "https://spdx.org/licenses/psfrag.html"), License("psutils", "psutils License", "https://spdx.org/licenses/psutils.html"), License("wxWindows", "wxWindows Library License", "https://spdx.org/licenses/wxWindows.html"), License("xinetd", "xinetd License", "https://spdx.org/licenses/xinetd.html"), License("xpp", "XPP License", "https://spdx.org/licenses/xpp.html"), License("zlib-acknowledgement", "zlib/libpng License with Acknowledgement", "https://spdx.org/licenses/zlib-acknowledgement.html") ) // format: on lazy val map = list.map(l => l.id -> l).toMap } ================================================ FILE: modules/core/src/main/scala/scala/build/internals/Name.scala ================================================ package scala.build.internal // adapted from https://github.com/com-lihaoyi/Ammonite/blob/9be39debc367abad5f5541ef58f4b986b2a8d045/amm/util/src/main/scala/ammonite/util/Model.scala#L45-L110 import scala.reflect.NameTransformer case class Name(raw: String) { assert( NameTransformer.decode(raw) == raw, "Name() must be created with un-encoded text" ) assert(raw.charAt(0) != '`', "Cannot create already-backticked identifiers") override def toString = s"Name($backticked)" def encoded = NameTransformer.encode(raw) def backticked = Name.backtickWrap(encoded) } object Name { def decoded(name: String) = NameTransformer.decode(name) val alphaKeywords = Set( "abstract", "case", "catch", "class", "def", "do", "else", "extends", "false", "finally", "final", "finally", "forSome", "for", "if", "implicit", "import", "lazy", "match", "new", "null", "object", "override", "package", "private", "protected", "return", "sealed", "super", "this", "throw", "trait", "try", "true", "type", "val", "var", "while", "with", "yield", "_", "macro" ) val symbolKeywords = Set( ":", ";", "=>", "=", "<-", "<:", "<%", ">:", "#", "@", "\u21d2", "\u2190" ) val blockCommentStart = "/*" val lineCommentStart = "//" /** Custom implementation of ID parsing, instead of using the ScalaParse version. This lets us * avoid loading FastParse and ScalaParse entirely if we're running a cached script, which shaves * off 200-300ms of startup time. */ def backtickWrap(s: String) = if (s.isEmpty) "``" else if (s(0) == '`' && s.last == '`') s else { val chunks = s.split("_", -1) def validOperator(c: Char) = c.getType == Character.MATH_SYMBOL || c.getType == Character.OTHER_SYMBOL || "!#%&*+-/:<=>?@\\^|~".contains(c) val validChunks = chunks.zipWithIndex.forall { case (chunk, index) => chunk.forall(c => c.isLetter || c.isDigit || c == '$') || ( chunk.forall(validOperator) && // operators can only come last index == chunks.length - 1 && // but cannot be preceded by only a _ !(chunks.lift(index - 1).exists(_ == "") && index - 1 == 0) ) } val firstLetterValid = s(0).isLetter || s(0) == '_' || s(0) == '$' || validOperator(s(0)) val valid = validChunks && firstLetterValid && !alphaKeywords.contains(s) && !symbolKeywords.contains(s) && !s.contains(blockCommentStart) && !s.contains(lineCommentStart) if (valid) s else "`" + s + '`' } } ================================================ FILE: modules/core/src/main/scala/scala/build/internals/NativeWrapper.scala ================================================ package scala.build.internals import scala.annotation.tailrec import scala.build.Logger import scala.io.Source import scala.util.Using /* * Invoke `native-image.exe` inside a vcvars-initialized cmd.exe session. * * A temp batch file calls `vcvars64.bat` to set up MSVC, then runs * `native-image.exe` directly in the same session. This avoids the * fragile pattern of capturing the vcvars environment via `set` and * replaying it through Java's ProcessBuilder, which silently loses * PATH entries on some JVM / Windows combinations. */ object MsvcEnvironment { // Lower threshold to ensure native-image's internal paths (which can add 100-130+ chars // for deeply nested source files) don't exceed Windows 260-char MAX_PATH limit. // Native-image creates paths like: native-sources\graal\com\oracle\svm\...\Target_ClassName.c private val pathLengthLimit = 90 /* * Call `native-image.exe` inside a vcvars-initialized cmd.exe session. * * Rather than capturing the vcvars environment and replaying it (which is * fragile — Java's ProcessBuilder env handling on Windows can silently lose * PATH entries set by vcvars64.bat), we write a small batch file that: * 1. calls vcvars64.bat (sets up MSVC in the session) * 2. runs native-image.exe directly (inherits the live session env) * * @return process exit code. */ def msvcNativeImageProcess( command: Seq[String], workingDir: os.Path, logger: Logger ): Int = { // Shorten the working dir for native-image (it creates deeply nested internal // paths that can exceed the Windows 260-char MAX_PATH limit). val (nativeImageWorkDir, driveToUnalias) = if (workingDir.toString.length >= pathLengthLimit) { val (driveLetter, shortPath) = getShortenedPath(workingDir, logger) (shortPath, Some(driveLetter)) } else (workingDir, None) try { val vcvOpt = vcvarsOpt(logger) vcvOpt match { case None => logger.debug(s"not found: vcvars64.bat") -1 case Some(vcvars) => logger.debug(s"Using vcvars script $vcvars") // show aliased drive map getSubstMappings.foreach((k, v) => logger.message(s"substMap $k: -> $v")) val vcvarsCmd = vcvars.toIO.getAbsolutePath // Replace native-image.cmd with native-image.exe, if applicable val updatedCommand: Seq[String] = command.headOption match { case Some(cmd) if cmd.toLowerCase.endsWith("native-image.cmd") => val cmdPath = os.Path(cmd, os.pwd) val graalHome = cmdPath / os.up / os.up resolveNativeImage(graalHome) match { case Some(exe) => exe.toString +: command.tail case None => command // fall back to the .cmd wrapper } case _ => command } // Quote arguments that contain batch-special characters val quotedArgs = updatedCommand.map { arg => if arg.exists(c => " &|^<>()".contains(c)) then s""""$arg"""" else arg }.mkString(" ") // Build a batch file that: // 1. calls vcvars64.bat (with the inherited, non-SUBST CWD) // 2. locates cl.exe and passes it explicitly to native-image // (works around GraalVM native-image not finding cl.exe via // PATH when the process runs from a SUBST-drive CWD) // 3. switches to the shortened SUBST working directory // 4. runs native-image.exe val batchContent = s"""@call "$vcvarsCmd" |@if errorlevel 1 exit /b %ERRORLEVEL% |@set GRAALVM_ARGUMENT_VECTOR_PROGRAM_NAME=native-image |@for /f "delims=" %%i in ('where cl.exe 2^>nul') do @set "CL_EXE=%%i" |@if not defined CL_EXE ( | echo cl.exe not found in PATH after vcvars 1>&2 | exit /b 1 |) |@cd /d "$nativeImageWorkDir" |@$quotedArgs --native-compiler-path="%CL_EXE%" |""".stripMargin val batchFile = os.temp(suffix = ".bat", contents = batchContent) logger.debug(s"native-image w/args: $updatedCommand") try // Don't pass cwd here — let cmd.exe inherit the parent's real // (non-SUBST) CWD so that vcvars64.bat runs without SUBST issues. // The batch file does `cd /d` to the shortened workdir before // launching native-image. val result = os.proc(cmdExe, "/c", batchFile.toString).call( stdout = os.Inherit, stderr = os.Inherit, check = false ) result.exitCode finally try os.remove(batchFile) catch { case _: Exception => } } } finally driveToUnalias.foreach(unaliasDriveLetter) } def getSubstMappings: Map[Char, String] = try val (exitCode, output) = execWindowsCmd(cmdExe, "/c", "subst") if exitCode != 0 then Map.empty else output .linesIterator .flatMap { line => // Example: "X:\: => C:\path\to\something" val parts = line.split("=>").map(_.trim) if parts.length == 2 then val drivePart = parts(0) // "X:\:" val target = parts(1) // "C:\path\to\something" // Extract the drive letter safely val maybeDrive: Option[Char] = if drivePart.length >= 2 && drivePart(1) == ':' then Some(drivePart(0)) // 'X' else None maybeDrive.map(_ -> target) else None } .toMap catch case _: Throwable => Map.empty // Reduce duplicate drive aliases to no more than one; // @return Some(single-alias) or None. private def consolidateAliases( targetPath: os.Path, logger: Logger ): Option[Char] = { val mappings = getSubstMappings val targetStr = targetPath.toString.toLowerCase // Find all drives pointing to our target (case-insensitive on Windows) val matchingDrives = mappings.filter { case (_, target) => target.toLowerCase == targetStr }.keys.toList.sorted matchingDrives match { case Nil => // No existing aliases for this target None case kept :: duples => // Keep first one, remove the rest duples.foreach { drive => logger.debug(s"Removing duplicate alias $drive: -> $targetStr") try unaliasDriveLetter(drive) catch { case e: Exception => logger.debug(s"Failed to remove duplicate alias $drive: ${e.getMessage}") } } if (duples.isEmpty) logger.debug(s"Reusing existing alias $kept: -> $targetStr") else logger.debug( s"Consolidated ${duples.size + 1} aliases to $kept:, removed: ${duples.mkString(", ")}" ) Some(kept) } } // Find or create a shortened alias for the given path def getShortenedPath( currentHome: os.Path, logger: Logger ): (Char, os.Path) = { val from = currentHome / os.up val driveLetter = consolidateAliases(from, logger) match { case Some(existingDrive) => existingDrive // Reuse existing alias case None => // Create new alias val driveLetter = availableDriveLetter() logger.debug(s"Creating drive alias $driveLetter: -> $from") aliasDriveLetter(driveLetter, from.toString) driveLetter } val drivePath = os.Path(s"$driveLetter:" + "\\") val newHome = drivePath / currentHome.last (driveLetter, newHome) } private def availableDriveLetter(): Char = { // if a drive letter has already been mapped by SUBST, it isn't free val substDrives: Set[Char] = getSubstMappings.keySet @tailrec def helper(from: Char): Char = if (from > 'Z') sys.error("Cannot find free drive letter") else if (mountedDrives.contains(from) || substDrives.contains(from)) helper((from + 1).toChar) else from helper('D') } def aliasDriveLetter(driveLetter: Char, from: String): Unit = execWindowsCmd(cmdExe, "/c", s"subst $driveLetter: \"$from\"") def unaliasDriveLetter(driveLetter: Char): Int = execWindowsCmd(cmdExe, "/c", s"subst $driveLetter: /d")._1 def execWindowsCmd(cmd: String*): (Int, String) = val pb = new ProcessBuilder(cmd*) pb.redirectInput(ProcessBuilder.Redirect.INHERIT) pb.redirectError(ProcessBuilder.Redirect.INHERIT) pb.redirectOutput(ProcessBuilder.Redirect.PIPE) val p = pb.start() // read stdout fully val output = Using(Source.fromInputStream(p.getInputStream, "UTF-8")) { source => source.mkString }.getOrElse("") val exitCode = p.waitFor() (exitCode, output) def setCodePage(cp: String): Int = execWindowsCmd(cmdExe, "/c", s"chcp $cp")._1 def getCodePage: String = { val out = execWindowsCmd(cmdExe, "/c", "chcp")._2 out.split(":").lastOption.map(_.trim).getOrElse("") // Extract the number } def getCodePage(logger: Logger): String = try { val out = os.proc(cmdExe, "/c", "chcp").call().out.text().trim out.split(":").lastOption.map(_.trim).getOrElse("") // Extract the number } catch { case e: Exception => logger.debug(s"unable to get initial code page: ${e.getMessage}") "" } private def resolveNativeImage(graalHome: os.Path): Option[os.Path] = { val candidates = Seq( graalHome / "lib" / "svm" / "bin" / "native-image.exe", graalHome / "bin" / "native-image.exe", graalHome / "native-image.exe" ) candidates.find(os.exists) } private def vcvarsOpt(logger: Logger): Option[os.Path] = { val candidates = vcVarsCandidates .iterator .map(os.Path(_, os.pwd)) .filter(os.exists(_)) .toSeq if (candidates.isEmpty) None else { // Sort lexicographically; newest VS installs always sort last val sorted = candidates.sortBy(_.toString) sorted.foreach(s => logger.debug(s"candidate: $s")) sorted.lastOption } } // newest VS first, Enterprise > Community > BuildTools private def vcVersions = Seq("2022", "2019", "2017") private def vcEditions = Seq("Enterprise", "Community", "BuildTools") private lazy val vcVarsCandidates: Iterable[String] = EnvVar.Misc.vcVarsAll.valueOpt ++ { for { isX86 <- Seq(false, true) version <- vcVersions edition <- vcEditions } yield { val programFiles = if (isX86) "Program Files (x86)" else "Program Files" """C:\""" + programFiles + """\Microsoft Visual Studio\""" + version + "\\" + edition + """\VC\Auxiliary\Build\vcvars64.bat""" } } lazy val mountedDrives: String = { val str = "HKEY_LOCAL_MACHINE/SYSTEM/MountedDevices".replace('/', '\\') val queryDrives = s"reg query $str" val lines = os.proc(cmdExe, "/c", queryDrives).call().out.lines() val dosDevices = lines.filter { s => s.contains("DosDevices") }.map { s => s.replaceAll(".DosDevices.", "").replaceAll(":.*", "") } dosDevices.mkString } lazy val systemRoot: String = sys.env.getOrElse("SystemRoot", "C:\\Windows").stripSuffix("\\") lazy val cmdExe: String = s"$systemRoot\\System32\\cmd.exe" } ================================================ FILE: modules/core/src/main/scala/scala/build/internals/OsLibc.scala ================================================ package scala.build.internal import bloop.rifle.VersionUtil.parseJavaVersion import coursier.jvm.JvmChannel import java.io.IOException import java.nio.charset.Charset import scala.build.Os import scala.util.{Properties, Try} object OsLibc { lazy val isMusl: Option[Boolean] = { def tryRun(cmd: String*): Option[os.CommandResult] = try { val res = os.proc(cmd).call( mergeErrIntoOut = true, check = false ) Some(res) } catch { case _: IOException => None } val getconfResOpt = tryRun("getconf", "GNU_LIBC_VERSION") if (getconfResOpt.exists(_.exitCode == 0)) Some(false) else { val lddResOpt = tryRun("ldd", "--version") val foundMusl = lddResOpt.exists { lddRes => (lddRes.exitCode == 0 || lddRes.exitCode == 1) && lddRes.out.text(Charset.defaultCharset()).contains("musl") } if (foundMusl) Some(true) else { val inLib = os.list(os.Path("/lib")).map(_.last) if (inLib.exists(_.contains("-linux-gnu"))) Some(false) else if (inLib.exists(name => name.contains("libc.musl-") || name.contains("ld-musl-"))) Some(true) else { val inUsrSbin = os.list(os.Path("/usr/sbin")).map(_.last) if (inUsrSbin.exists(_.contains("glibc"))) Some(false) else None } } } } // FIXME These values should be the default ones in coursier-jvm lazy val jvmIndexOs: String = { val default = JvmChannel.defaultOs if (default == "linux" && isMusl.getOrElse(false)) "linux-musl" else default } def baseDefaultJvm(os: String, jvmVersion: String): String = { def bloomMinimumJavaOrHigher = Try(jvmVersion.takeWhile(_.isDigit).toInt) .toOption .forall(_ >= Constants.minimumBloopJavaVersion) if (os == "linux-musl") s"liberica:$jvmVersion" // zulu could work too else if (bloomMinimumJavaOrHigher) s"temurin:$jvmVersion" else if (Os.isArmArchitecture) s"zulu:$jvmVersion" // adopt doesn't support Java 8 on macOS arm else s"temurin:$jvmVersion" } def defaultJvm(os: String): String = baseDefaultJvm(os, Constants.defaultJavaVersion.toString) def javaVersion(javaCmd: String): Int = { val javaVersionOutput = os.proc(javaCmd, "-version").call( cwd = os.pwd, stdout = os.Pipe, stderr = os.Pipe, mergeErrIntoOut = true ).out.trim() parseJavaVersion(javaVersionOutput).getOrElse { throw new Exception(s"Could not parse java version from output: $javaVersionOutput") } } def javaHomeVersion(javaHome: os.Path): (Int, String) = { val ext = if (Properties.isWin) ".exe" else "" val javaCmd = (javaHome / "bin" / s"java$ext").toString (javaVersion(javaCmd), javaCmd) } } ================================================ FILE: modules/core/src/main/scala/scala/build/internals/Regexes.scala ================================================ package scala.build.internal object Regexes { val scala2NightlyRegex = raw"""2\.(\d+)\.(\d+)-bin-[a-f0-9]*""".r val scala3NightlyNicknameRegex = raw"""3\.([0-9]*)\.nightly""".r val scala3RcRegex = raw"""3\.([0-9]*\.[0-9]*-[rR][cC][0-9]+)""".r val scala3RcNicknameRegex = raw"""3\.([0-9]*)\.?[rR][cC]""".r val scala3LtsRegex = raw"""3\.3\.[0-9]+""".r } ================================================ FILE: modules/core/src/main/scala/scala/build/internals/StableScalaVersion.scala ================================================ package scala.build.internal import com.github.plokhotnyuk.jsoniter_scala.core.* import com.github.plokhotnyuk.jsoniter_scala.macros.* import coursier.core.Version final case class StableScalaVersion( scalaCliVersion: String, supportedScalaVersions: Seq[String] ) { lazy val scalaCliVersion0 = Version(scalaCliVersion) } object StableScalaVersion { val seqCodec: JsonValueCodec[Seq[StableScalaVersion]] = JsonCodecMaker.make } ================================================ FILE: modules/core/src/main/scala/scala/build/warnings/DeprecatedWarning.scala ================================================ package scala.build.warnings import scala.build.Position import scala.build.errors.Diagnostic.TextEdit import scala.build.errors.{Diagnostic, Severity} final case class DeprecatedWarning( message: String, positions: Seq[Position], override val textEdit: Option[TextEdit] ) extends Diagnostic { def severity: Severity = Severity.Warning } ================================================ FILE: modules/directives/src/main/scala/scala/build/directives/DirectiveDescription.scala ================================================ package scala.build.directives import scala.annotation.StaticAnnotation final case class DirectiveDescription(description: String, descriptionMd: String = "") extends StaticAnnotation ================================================ FILE: modules/directives/src/main/scala/scala/build/directives/DirectiveExamples.scala ================================================ package scala.build.directives import scala.annotation.StaticAnnotation final case class DirectiveExamples(examples: String) extends StaticAnnotation ================================================ FILE: modules/directives/src/main/scala/scala/build/directives/DirectiveGroupDetails.scala ================================================ package scala.build.directives final case class DirectiveGroupDetails( name: String, description: String, usage: String, descriptionMdOpt: Option[String] = None, usageMdOpt: Option[String] = None, examples: Seq[String] = Nil ) { def descriptionMd: String = descriptionMdOpt.getOrElse(description) def usageMd: String = usageMdOpt.getOrElse(s"`$usage`") } ================================================ FILE: modules/directives/src/main/scala/scala/build/directives/DirectiveGroupName.scala ================================================ package scala.build.directives import scala.annotation.StaticAnnotation final case class DirectiveGroupName(name: String) extends StaticAnnotation ================================================ FILE: modules/directives/src/main/scala/scala/build/directives/DirectiveLevel.scala ================================================ package scala.build.directives import scala.annotation.StaticAnnotation import scala.cli.commands.SpecificationLevel final case class DirectiveLevel(level: SpecificationLevel) extends StaticAnnotation ================================================ FILE: modules/directives/src/main/scala/scala/build/directives/DirectiveName.scala ================================================ package scala.build.directives import scala.annotation.StaticAnnotation final case class DirectiveName(name: String) extends StaticAnnotation ================================================ FILE: modules/directives/src/main/scala/scala/build/directives/DirectiveSpecialSyntax.scala ================================================ package scala.build.directives import scala.util.matching.Regex object DirectiveSpecialSyntax { /** Replaces the `${.}` pattern in the directive value with the parent directory of the file * containing the directive. Skips replacement if the pattern is preceded by two dollar signs * ($$). https://github.com/VirtusLab/scala-cli/issues/1098 * * @param directiveValue * the value of the directive, e.g., "-coverage-out:${.}" for example for the directive "//> * using options "-coverage-out:${.}"" * @param path * the file path from which the directive is read; replacement occurs only if the directive is * from a local file * @return * the directive value with the `${.}` pattern replaced by the parent directory, if applicable */ def handlingSpecialPathSyntax(directiveValue: String, path: Either[String, os.Path]): String = { val pattern = """(((?:\$)+)(\{\.\}))""".r path match { case Right(p) => pattern.replaceAllIn( directiveValue, (m: Regex.Match) => { val dollarSigns = m.group(2) val dollars = "\\$" * (dollarSigns.length / 2) if (dollarSigns.length % 2 == 0) s"$dollars${m.group(3)}" else s"$dollars${p / os.up}" } ) case _ => directiveValue } } } ================================================ FILE: modules/directives/src/main/scala/scala/build/directives/DirectiveUsage.scala ================================================ package scala.build.directives import scala.annotation.StaticAnnotation final case class DirectiveUsage(usage: String, usageMd: String = "") extends StaticAnnotation ================================================ FILE: modules/directives/src/main/scala/scala/build/directives/DirectiveValueParser.scala ================================================ package scala.build.directives import com.virtuslab.using_directives.custom.model.{BooleanValue, EmptyValue, StringValue, Value} import scala.build.errors.{ BuildException, CompositeBuildException, MalformedDirectiveError, ToolkitDirectiveMissingVersionError, UsingDirectiveValueNumError, UsingDirectiveWrongValueTypeError } import scala.build.preprocessing.ScopePath import scala.build.preprocessing.directives.DirectiveUtil import scala.build.{Position, Positioned} abstract class DirectiveValueParser[+T] { def parse( key: String, values: Seq[Value[?]], scopePath: ScopePath, path: Either[String, os.Path] ): Either[BuildException, T] final def map[U](f: T => U): DirectiveValueParser[U] = new DirectiveValueParser.Mapped[T, U](this, f) } object DirectiveValueParser { private final class Mapped[T, +U](underlying: DirectiveValueParser[T], f: T => U) extends DirectiveValueParser[U] { def parse( key: String, values: Seq[Value[?]], scopePath: ScopePath, path: Either[String, os.Path] ): Either[BuildException, U] = underlying.parse(key, values, scopePath, path).map(f) } abstract class DirectiveSingleValueParser[+T] extends DirectiveValueParser[T] { def parseValue( key: String, value: Value[?], cwd: ScopePath, path: Either[String, os.Path] ): Either[BuildException, T] final def parse( key: String, values: Seq[Value[?]], scopePath: ScopePath, path: Either[String, os.Path] ): Either[BuildException, T] = values match { case Seq(value) if !value.isEmpty => parseValue(key, value, scopePath, path) case Seq(value) if value.isEmpty && (key == "toolkit" || key == "test.toolkit") => // FIXME: handle similar parsing errors in the directive declaration instead of hacks like this one Left(ToolkitDirectiveMissingVersionError(maybePath = path, key = key)) case resultValues @ _ => Left( new UsingDirectiveValueNumError( maybePath = path, key = key, expectedValueNum = 1, providedValueNum = resultValues.count(!_.isEmpty) ) ) } } given DirectiveValueParser[Unit] = { (_, values, _, path) => values match { case Seq() => Right(()) case Seq(value, _*) => val pos = value.position(path) Left(new MalformedDirectiveError("Expected no value in directive", Seq(pos))) } } extension (value: Value[?]) { def isEmpty: Boolean = value match { case _: EmptyValue => true case _ => false } def isString: Boolean = value match { case _: StringValue => true case _ => false } def asString: Option[String] = value match { case s: StringValue => Some(s.get()) case _ => None } def isBoolean: Boolean = value match { case _: BooleanValue => true case _ => false } def asBoolean: Option[Boolean] = value match { case s: BooleanValue => Some(s.get()) case _ => None } def position(path: Either[String, os.Path]): Position = DirectiveUtil.position(value, path) } given DirectiveValueParser[Boolean] = { (key, values, _, path) => values.filter(!_.isEmpty) match { case Seq() => Right(true) case Seq(v) => v.asBoolean.toRight { new UsingDirectiveWrongValueTypeError( maybePath = path, key = key, expectedTypes = Seq("boolean"), hint = "" ) } case values0 => Left( new MalformedDirectiveError( s"Unexpected values ${values0.map(_.toString).mkString(", ")}", values0.map(_.position(path)) ) ) } } given DirectiveSingleValueParser[String] = (key, value, _, path) => value.asString.toRight { val pos = value.position(path) new MalformedDirectiveError( message = s"""Encountered an error for the $key using directive. |Expected a string, got '${value.getRelatedASTNode.toString}'""".stripMargin, positions = Seq(pos) ) }.map(DirectiveSpecialSyntax.handlingSpecialPathSyntax(_, path)) final case class MaybeNumericalString(value: String) given DirectiveSingleValueParser[MaybeNumericalString] = (key, value, _, path) => value.asString.map(MaybeNumericalString(_)).toRight { val pos = value.position(path) new MalformedDirectiveError( s"""Encountered an error for the $key using directive. |Expected a string value, got '${value.getRelatedASTNode.toString}'""".stripMargin, Seq(pos) ) } final case class WithScopePath[+T](scopePath: ScopePath, value: T) object WithScopePath { def empty[T](value: T): WithScopePath[T] = WithScopePath(ScopePath(Left("invalid"), os.sub), value) } given [T](using underlying: DirectiveValueParser[T]): DirectiveValueParser[WithScopePath[T]] = { (key, values, scopePath, path) => underlying.parse(key, values, scopePath, path) .map(WithScopePath(scopePath, _)) } given [T](using underlying: DirectiveSingleValueParser[T] ): DirectiveSingleValueParser[Positioned[T]] = { (key, value, scopePath, path) => underlying.parseValue(key, value, scopePath, path) .map(Positioned(value.position(path), _)) } given [T](using underlying: DirectiveValueParser[T]): DirectiveValueParser[Option[T]] = underlying.map(Some(_)) given [T](using underlying: DirectiveSingleValueParser[T]): DirectiveValueParser[List[T]] = { (key, values, scopePath, path) => val res = values.filter(!_.isEmpty).map(underlying.parseValue(key, _, scopePath, path)) val errors = res.collect { case Left(e) => e } if (errors.isEmpty) Right(res.collect { case Right(v) => v }.toList) else Left(CompositeBuildException(errors)) } } ================================================ FILE: modules/directives/src/main/scala/scala/build/directives/HasBuildOptions.scala ================================================ package scala.build.directives import scala.build.errors.BuildException import scala.build.options.BuildOptions trait HasBuildOptions { def buildOptions: Either[BuildException, BuildOptions] } ================================================ FILE: modules/directives/src/main/scala/scala/build/directives/HasBuildOptionsWithRequirements.scala ================================================ package scala.build.directives import scala.build.Ops.* import scala.build.errors.{BuildException, CompositeBuildException} import scala.build.options.{BuildOptions, WithBuildRequirements} trait HasBuildOptionsWithRequirements { def buildOptionsList: List[Either[BuildException, WithBuildRequirements[BuildOptions]]] final def buildOptionsWithRequirements : Either[BuildException, List[WithBuildRequirements[BuildOptions]]] = buildOptionsList .sequence .left.map(CompositeBuildException(_)) .map(_.toList) } ================================================ FILE: modules/directives/src/main/scala/scala/build/directives/HasBuildRequirements.scala ================================================ package scala.build.directives import scala.build.errors.BuildException import scala.build.options.BuildRequirements trait HasBuildRequirements { def buildRequirements: Either[BuildException, BuildRequirements] } ================================================ FILE: modules/directives/src/main/scala/scala/build/directives/ScopedValue.scala ================================================ package scala.build.preprocessing.directives import com.virtuslab.using_directives.custom.model.Value import scala.build.Positioned import scala.build.preprocessing.ScopePath case class ScopedValue[T <: Value[?]]( positioned: Positioned[String], maybeScopePath: Option[ScopePath] = None ) ================================================ FILE: modules/directives/src/main/scala/scala/build/errors/ScalaJsLinkingError.scala ================================================ package scala.build.errors final class ScalaJsLinkingError extends BuildException("Error linking Scala.js") ================================================ FILE: modules/directives/src/main/scala/scala/build/errors/SingleValueExpectedError.scala ================================================ package scala.build.errors import scala.build.preprocessing.directives.{DirectiveUtil, StrictDirective} final class SingleValueExpectedError( val directive: StrictDirective, val path: Either[String, os.Path] ) extends BuildException( s"Expected a single value for directive ${directive.key} " + s"(got ${directive.values.length} values: ${directive.values.map(_.get().toString).mkString(", ")})", positions = DirectiveUtil.positions(directive.values, path) ) { assert(directive.stringValuesCount > 1) } ================================================ FILE: modules/directives/src/main/scala/scala/build/errors/UsingDirectiveExpectationError.scala ================================================ package scala.build.errors sealed abstract class UsingDirectiveExpectationError( message: String ) extends BuildException(message) final class UsingDirectiveWrongValueTypeError( maybePath: Either[String, os.Path], key: String, expectedTypes: Seq[String], hint: String = "" ) extends UsingDirectiveExpectationError( s"""${expectedTypes.mkString( ", or " )} expected for the $key using directive key${maybePath.map(path => s" at $path").getOrElse( "" )}. |$hint""".stripMargin ) final class UsingDirectiveValueNumError( maybePath: Either[String, os.Path], key: String, expectedValueNum: Int, providedValueNum: Int ) extends UsingDirectiveExpectationError({ val pathString = maybePath.map(p => s" at $p").getOrElse("") s"""Encountered an error when parsing the `$key` using directive$pathString. |Expected $expectedValueNum values, but got $providedValueNum values instead.""".stripMargin }) final class ToolkitDirectiveMissingVersionError( val maybePath: Either[String, os.Path], val key: String ) extends UsingDirectiveExpectationError({ val pathString = maybePath.map(p => s" at $p").getOrElse("") s"""Encountered an error when parsing the `$key` using directive$pathString. |Expected a version or "default" to be passed. |Example: `//> using $key default` |""".stripMargin }) object ToolkitDirectiveMissingVersionError { def apply(maybePath: Either[String, os.Path], key: String): ToolkitDirectiveMissingVersionError = new ToolkitDirectiveMissingVersionError(maybePath, key) def unapply(arg: ToolkitDirectiveMissingVersionError): (Either[String, os.Path], String) = arg.maybePath -> arg.key } ================================================ FILE: modules/directives/src/main/scala/scala/build/errors/UsingFileFromUriError.scala ================================================ package scala.build.errors import java.net.URI import scala.build.Position final class UsingFileFromUriError(uri: URI, positions: Seq[Position], description: String) extends BuildException( message = s"Error using file from $uri - $description", positions = positions ) ================================================ FILE: modules/directives/src/main/scala/scala/build/errors/WrongDirectoryPathError.scala ================================================ package scala.build.errors class WrongDirectoryPathError(cause: Throwable) extends BuildException( message = s"""The directory path argument in the using directives at could not be found! |${cause.getLocalizedMessage}""".stripMargin, cause = cause ) ================================================ FILE: modules/directives/src/main/scala/scala/build/errors/WrongJarPathError.scala ================================================ package scala.build.errors class WrongJarPathError(cause: Throwable) extends BuildException( message = s"""The jar path argument in the using directives at could not be found! |${cause.getLocalizedMessage}""".stripMargin, cause = cause ) ================================================ FILE: modules/directives/src/main/scala/scala/build/errors/WrongJavaHomePathError.scala ================================================ package scala.build.errors class WrongJavaHomePathError(javaHomeValue: String, cause: Throwable) extends BuildException( message = s"""The java home path argument in the using directives at $javaHomeValue could not be found! |${cause.getLocalizedMessage}""".stripMargin, cause = cause ) ================================================ FILE: modules/directives/src/main/scala/scala/build/errors/WrongSourcePathError.scala ================================================ package scala.build.errors import scala.build.Position class WrongSourcePathError(path: String, cause: Throwable, positions: Seq[Position]) extends BuildException( message = s"Invalid path argument '$path' in using directives".stripMargin, cause = cause, positions = positions ) ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/ScopePath.scala ================================================ package scala.build.preprocessing final case class ScopePath( root: Either[String, os.Path], subPath: os.SubPath ) { def /(subPath: os.PathChunk): ScopePath = copy(subPath = this.subPath / subPath) def path: Either[String, os.Path] = root .left.map(r => s"$r/$subPath") .map(_ / subPath) } object ScopePath { def fromPath(path: os.Path): ScopePath = { def root(p: os.Path): os.Path = if (p.segmentCount > 0) root(p / os.up) else p val root0 = root(path) ScopePath(Right(root0), path.subRelativeTo(root0)) } } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/Scoped.scala ================================================ package scala.build.preprocessing import scala.build.errors.BuildException final case class Scoped[+T](path: ScopePath, value: T) { def appliesTo(candidate: ScopePath): Boolean = path.root == candidate.root && candidate.subPath.startsWith(path.subPath) def valueFor(candidate: ScopePath): Option[T] = if (appliesTo(candidate)) Some(value) else None def map[U](f: T => U): Scoped[U] = copy(value = f(value)) def mapE[U](f: T => Either[BuildException, U]): Either[BuildException, Scoped[U]] = f(value).map(u => copy(value = u)) } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/Benchmarking.scala ================================================ package scala.build.preprocessing.directives import scala.build.Positioned import scala.build.directives.* import scala.build.errors.BuildException import scala.build.internal.Constants import scala.build.options.{BuildOptions, JmhOptions} import scala.cli.commands.SpecificationLevel @DirectiveGroupName("Benchmarking options") @DirectiveExamples(s"//> using jmh") @DirectiveExamples(s"//> using jmh true") @DirectiveExamples(s"//> using jmhVersion ${Constants.jmhVersion}") @DirectiveUsage( "//> using jmh _value_ | using jmhVersion _value_", """`//> using jmh` _value_ | |`//> using jmhVersion` _value_ """.stripMargin.trim ) @DirectiveDescription("Add benchmarking options") @DirectiveLevel(SpecificationLevel.EXPERIMENTAL) case class Benchmarking( jmh: Option[Boolean] = None, jmhVersion: Option[Positioned[String]] = None ) extends HasBuildOptions { def buildOptions: Either[BuildException, BuildOptions] = Right( BuildOptions( jmhOptions = JmhOptions( enableJmh = jmh, runJmh = jmh, jmhVersion = jmhVersion.map(_.value) ) ) ) } object Benchmarking { val handler: DirectiveHandler[Benchmarking] = DirectiveHandler.derive } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/BuildInfo.scala ================================================ package scala.build.preprocessing.directives import scala.build.directives.* import scala.build.errors.BuildException import scala.build.options.{BuildOptions, SourceGeneratorOptions} import scala.cli.commands.SpecificationLevel @DirectiveExamples("//> using buildInfo") @DirectiveUsage("//> using buildInfo", "`//> using buildInfo`") @DirectiveDescription("Generate BuildInfo for project") @DirectiveLevel(SpecificationLevel.RESTRICTED) final case class BuildInfo( buildInfo: Boolean = false ) extends HasBuildOptions { def buildOptions: Either[BuildException, BuildOptions] = val options = BuildOptions(sourceGeneratorOptions = SourceGeneratorOptions(useBuildInfo = Some(buildInfo)) ) Right(options) } object BuildInfo { val handler: DirectiveHandler[BuildInfo] = DirectiveHandler.derive } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/ClasspathUtils.scala ================================================ package scala.build.preprocessing.directives object ClasspathUtils { extension (classpathItem: os.Path) { def hasSourceJarSuffix: Boolean = classpathItem.last.endsWith("-sources.jar") } } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/ComputeVersion.scala ================================================ package scala.build.preprocessing.directives import scala.build.EitherCps.{either, value} import scala.build.Ops.EitherOptOps import scala.build.Positioned import scala.build.directives.{ DirectiveDescription, DirectiveExamples, DirectiveGroupName, DirectiveLevel, DirectiveUsage, HasBuildOptions } import scala.build.errors.BuildException import scala.build.options.{BuildOptions, ComputeVersion as cv, SourceGeneratorOptions} import scala.cli.commands.SpecificationLevel @DirectiveGroupName("Compute Version") @DirectiveExamples("//> using computeVersion git") @DirectiveExamples("//> using computeVersion git:tag") @DirectiveExamples("//> using computeVersion git:dynver") @DirectiveUsage("//> using computeVersion git:tag", "`//> using computeVersion` _method_") @DirectiveDescription("Method used to compute the version for BuildInfo") @DirectiveLevel(SpecificationLevel.RESTRICTED) final case class ComputeVersion(computeVersion: Option[Positioned[String]] = None) extends HasBuildOptions { def buildOptions: Either[BuildException, BuildOptions] = either { BuildOptions( sourceGeneratorOptions = SourceGeneratorOptions( computeVersion = value { computeVersion .map(cv.parse) .sequence } ) ) } } object ComputeVersion { val handler: DirectiveHandler[ComputeVersion] = DirectiveHandler.derive } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/CustomJar.scala ================================================ package scala.build.preprocessing.directives import scala.build.Ops.* import scala.build.Positioned import scala.build.directives.* import scala.build.errors.{BuildException, CompositeBuildException, WrongJarPathError} import scala.build.options.WithBuildRequirements.* import scala.build.options.{BuildOptions, ClassPathOptions, Scope, WithBuildRequirements} import scala.build.preprocessing.directives.ClasspathUtils.* import scala.build.preprocessing.directives.CustomJar.JarType import scala.cli.commands.SpecificationLevel import scala.util.Try @DirectiveGroupName("Custom JAR") @DirectiveExamples( "//> using jar /Users/alexandre/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/com/chuusai/shapeless_2.13/2.3.7/shapeless_2.13-2.3.7.jar" ) @DirectiveExamples( "//> using test.jar /Users/alexandre/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/com/chuusai/shapeless_2.13/2.3.7/shapeless_2.13-2.3.7.jar" ) @DirectiveExamples("//> using sourceJar /path/to/custom-jar-sources.jar") @DirectiveExamples( "//> using sourceJars /path/to/custom-jar-sources.jar /path/to/another-jar-sources.jar" ) @DirectiveExamples("//> using test.sourceJar /path/to/test-custom-jar-sources.jar") @DirectiveUsage( "`//> using jar `_path_ | `//> using jars `_path1_ _path2_ …", """`//> using jar` _path_ | |`//> using jars` _path1_ _path2_ … | |`//> using test.jar` _path_ | |`//> using test.jars` _path1_ _path2_ … | |`//> using source.jar` _path_ | |`//> using source.jars` _path1_ _path2_ … | |`//> using test.source.jar` _path_ | |`//> using test.source.jars` _path1_ _path2_ … |""".stripMargin ) @DirectiveDescription("Manually add JAR(s) to the class path") @DirectiveLevel(SpecificationLevel.SHOULD) final case class CustomJar( @DirectiveName("jars") jar: DirectiveValueParser.WithScopePath[List[Positioned[String]]] = DirectiveValueParser.WithScopePath.empty(Nil), @DirectiveName("test.jar") @DirectiveName("test.jars") testJar: DirectiveValueParser.WithScopePath[List[Positioned[String]]] = DirectiveValueParser.WithScopePath.empty(Nil), @DirectiveName("sources.jar") @DirectiveName("sourcesJars") @DirectiveName("sources.jars") @DirectiveName("sourceJar") @DirectiveName("source.jar") @DirectiveName("sourceJars") @DirectiveName("source.jars") sourcesJar: DirectiveValueParser.WithScopePath[List[Positioned[String]]] = DirectiveValueParser.WithScopePath.empty(Nil), @DirectiveName("test.sourcesJar") @DirectiveName("test.sources.jar") @DirectiveName("test.sourcesJars") @DirectiveName("test.sources.jars") @DirectiveName("test.sourceJar") @DirectiveName("test.source.jar") @DirectiveName("test.sourceJars") @DirectiveName("test.source.jars") testSourcesJar: DirectiveValueParser.WithScopePath[List[Positioned[String]]] = DirectiveValueParser.WithScopePath.empty(Nil) ) extends HasBuildOptionsWithRequirements { def buildOptionsList: List[Either[BuildException, WithBuildRequirements[BuildOptions]]] = List( CustomJar.buildOptions(jar, JarType.Jar) .map(_.withEmptyRequirements), CustomJar.buildOptions(testJar, JarType.Jar) .map(_.withScopeRequirement(Scope.Test)), CustomJar.buildOptions(sourcesJar, JarType.SourcesJar) .map(_.withEmptyRequirements), CustomJar.buildOptions(testSourcesJar, JarType.SourcesJar) .map(_.withScopeRequirement(Scope.Test)) ) } object CustomJar { val handler: DirectiveHandler[CustomJar] = DirectiveHandler.derive enum JarType: case Jar, SourcesJar def buildOptions( jar: DirectiveValueParser.WithScopePath[List[Positioned[String]]], jarType: JarType ): Either[BuildException, BuildOptions] = { val cwd = jar.scopePath jar.value .map { posPathStr => val eitherRootPathOrBuildException = Directive.osRoot(cwd, posPathStr.positions.headOption) eitherRootPathOrBuildException.flatMap { root => Try(os.Path(posPathStr.value, root)) .toEither .left.map(new WrongJarPathError(_)) } } .sequence .left.map(CompositeBuildException(_)) .map { paths => val classPathOptions = jarType match case JarType.Jar => val (sourceJars, regularJars) = paths.partition(_.hasSourceJarSuffix) ClassPathOptions(extraClassPath = regularJars, extraSourceJars = sourceJars) case JarType.SourcesJar => ClassPathOptions(extraSourceJars = paths) BuildOptions(classPathOptions = classPathOptions) } } } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/Dependency.scala ================================================ package scala.build.preprocessing.directives import dependency.AnyDependency import scala.build.EitherCps.{either, value} import scala.build.Positioned import scala.build.directives.* import scala.build.errors.BuildException import scala.build.options.WithBuildRequirements.* import scala.build.options.{ BuildOptions, ClassPathOptions, Scope, ShadowingSeq, WithBuildRequirements } import scala.build.preprocessing.directives.Dependency.DependencyType import scala.build.preprocessing.directives.DirectiveUtil.* import scala.cli.commands.SpecificationLevel @DirectiveExamples("//> using dep com.lihaoyi::os-lib:0.9.1") @DirectiveExamples( "//> using dep tabby:tabby:0.2.3,url=https://github.com/bjornregnell/tabby/releases/download/v0.2.3/tabby_3-0.2.3.jar" ) @DirectiveExamples("//> using test.dep org.scalatest::scalatest:3.2.10") @DirectiveExamples("//> using test.dep org.scalameta::munit:0.7.29") @DirectiveExamples( "//> using compileOnly.dep com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros:2.23.2" ) @DirectiveExamples( "//> using scalafix.dep com.github.xuwei-k::scalafix-rules:0.5.1" ) @DirectiveUsage( "//> using dep org:name:ver | //> using deps org:name:ver org2:name2:ver2", """`//> using dep` _org_`:`name`:`ver | |`//> using deps` _org_`:`name`:`ver _org_`:`name`:`ver | |`//> using dependencies` _org_`:`name`:`ver _org_`:`name`:`ver | |`//> using test.dep` _org_`:`name`:`ver | |`//> using test.deps` _org_`:`name`:`ver _org_`:`name`:`ver | |`//> using test.dependencies` _org_`:`name`:`ver _org_`:`name`:`ver | |`//> using compileOnly.dep` _org_`:`name`:`ver | |`//> using compileOnly.deps` _org_`:`name`:`ver _org_`:`name`:`ver | |`//> using compileOnly.dependencies` _org_`:`name`:`ver _org_`:`name`:`ver | |`//> using scalafix.dep` _org_`:`name`:`ver | |`//> using scalafix.deps` _org_`:`name`:`ver _org_`:`name`:`ver | |`//> using scalafix.dependencies` _org_`:`name`:`ver _org_`:`name`:`ver |""".stripMargin ) @DirectiveDescription("Add dependencies") @DirectiveLevel(SpecificationLevel.MUST) final case class Dependency( @DirectiveName("lib") // backwards compat @DirectiveName("libs") // backwards compat @DirectiveName("dep") @DirectiveName("deps") @DirectiveName("dependencies") dependency: List[Positioned[String]] = Nil, @DirectiveName("test.dependency") @DirectiveName("test.dep") @DirectiveName("test.deps") @DirectiveName("test.dependencies") testDependency: List[Positioned[String]] = Nil, @DirectiveName("compileOnly.lib") // backwards compat @DirectiveName("compileOnly.libs") // backwards compat @DirectiveName("compileOnly.dep") @DirectiveName("compileOnly.deps") @DirectiveName("compileOnly.dependencies") compileOnlyDependency: List[Positioned[String]] = Nil, @DirectiveName("scalafix.dep") @DirectiveName("scalafix.deps") @DirectiveName("scalafix.dependencies") scalafixDependency: List[Positioned[String]] = Nil ) extends HasBuildOptionsWithRequirements { def buildOptionsList: List[Either[BuildException, WithBuildRequirements[BuildOptions]]] = List( Dependency.buildOptions(dependency, DependencyType.Runtime).map(_.withEmptyRequirements), Dependency.buildOptions(testDependency, DependencyType.Runtime).map( _.withScopeRequirement(Scope.Test) ), Dependency.buildOptions(compileOnlyDependency, DependencyType.CompileOnly) .map(_.withEmptyRequirements), Dependency.buildOptions(scalafixDependency, DependencyType.Scalafix) .map(_.withEmptyRequirements) ) } object Dependency { val handler: DirectiveHandler[Dependency] = DirectiveHandler.derive sealed trait DependencyType object DependencyType { case object Runtime extends DependencyType case object CompileOnly extends DependencyType case object Scalafix extends DependencyType } def buildOptions( ds: List[Positioned[String]], tpe: DependencyType ): Either[BuildException, BuildOptions] = either { val dependencies: ShadowingSeq[Positioned[AnyDependency]] = value(ds.asDependencies.map(ShadowingSeq.from)) val classPathOptions = tpe match { case DependencyType.Runtime => ClassPathOptions(extraDependencies = dependencies) case DependencyType.CompileOnly => ClassPathOptions(extraCompileOnlyDependencies = dependencies) case DependencyType.Scalafix => ClassPathOptions(scalafixDependencies = dependencies) } BuildOptions(classPathOptions = classPathOptions) } } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/Directive.scala ================================================ package scala.build.preprocessing.directives import scala.build.Position import scala.build.errors.{BuildException, ForbiddenPathReferenceError} import scala.build.preprocessing.ScopePath final case class Directive( tpe: Directive.Type, values: Seq[String], scope: Option[String], isComment: Boolean, position: Position ) object Directive { sealed abstract class Type(val name: String) extends Product with Serializable case object Using extends Type("using") case object Require extends Type("require") def osRootResource(cwd: ScopePath): (Option[os.SubPath], Option[os.Path]) = cwd.root match { case Left(_) => (Some(cwd.subPath), None) case Right(root) => (None, Some(root / cwd.subPath)) } def osRoot(cwd: ScopePath, pos: Option[Position]): Either[BuildException, os.Path] = cwd.root match { case Left(virtualRoot) => Left(new ForbiddenPathReferenceError(virtualRoot, pos)) case Right(root) => Right(root / cwd.subPath) } } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/DirectiveHandler.scala ================================================ package scala.build.preprocessing.directives import java.util.Locale import scala.build.Logger import scala.build.Ops.* import scala.build.directives.* import scala.build.errors.{BuildException, CompositeBuildException, UnexpectedDirectiveError} import scala.build.preprocessing.Scoped import scala.cli.commands.SpecificationLevel import scala.quoted.* trait DirectiveHandler[+T] { self => def name: String def description: String def descriptionMd: String = description def usage: String def usageMd: String = s"`$usage`" def examples: Seq[String] = Nil /** Is this directive an advanved feature, that will not be accessible when running scala-cli as * `scala` */ def scalaSpecificationLevel: SpecificationLevel protected def SpecificationLevel = scala.cli.commands.SpecificationLevel final def isRestricted: Boolean = scalaSpecificationLevel == SpecificationLevel.RESTRICTED final def isExperimental: Boolean = scalaSpecificationLevel == SpecificationLevel.EXPERIMENTAL def keys: Seq[Key] def handleValues( scopedDirective: ScopedDirective, logger: Logger ): Either[BuildException, ProcessedDirective[T]] def map[U](f: T => U): DirectiveHandler[U] = new DirectiveHandler[U] { def name = self.name def usage = self.usage override def usageMd = self.usageMd def description = self.description override def descriptionMd = self.descriptionMd override def examples = self.examples def scalaSpecificationLevel = self.scalaSpecificationLevel def keys = self.keys def handleValues(scopedDirective: ScopedDirective, logger: Logger) = self.handleValues(scopedDirective, logger) .map(_.map(f)) } def mapE[U](f: T => Either[BuildException, U]): DirectiveHandler[U] = new DirectiveHandler[U] { def name = self.name def usage = self.usage override def usageMd = self.usageMd def description = self.description override def descriptionMd = self.descriptionMd override def examples = self.examples def scalaSpecificationLevel = self.scalaSpecificationLevel def keys = self.keys def handleValues(scopedDirective: ScopedDirective, logger: Logger) = self.handleValues(scopedDirective, logger).flatMap(_.mapE(f)) } } /** Using directive key with all its aliases */ case class Key(nameAliases: Seq[String]) object DirectiveHandler { // from https://github.com/alexarchambault/case-app/blob/7ac9ae7cc6765df48eab27c4e35c66b00e4469a7/core/shared/src/main/scala/caseapp/core/util/CaseUtil.scala#L5-L22 def pascalCaseSplit(s: List[Char]): List[String] = if (s.isEmpty) Nil else if (!s.head.isUpper) { val (w, tail) = s.span(!_.isUpper) w.mkString :: pascalCaseSplit(tail) } else if (s.tail.headOption.forall(!_.isUpper)) { val (w, tail) = s.tail.span(!_.isUpper) (s.head :: w).mkString :: pascalCaseSplit(tail) } else { val (w, tail) = s.span(_.isUpper) if (tail.isEmpty) w.mkString :: pascalCaseSplit(tail) else w.init.mkString :: pascalCaseSplit(w.last :: tail) } def normalizeName(s: String): String = { val elems = s.split('-') (elems.head +: elems.tail.map(_.capitalize)).mkString } private def fields[U](using q: Quotes, t: Type[U] ): List[(q.reflect.Symbol, q.reflect.TypeRepr)] = { import quotes.reflect.* val sym = TypeRepr.of[U] match { case AppliedType(base, _) => base.typeSymbol case _ => TypeTree.of[U].symbol } // Many things inspired by https://github.com/plokhotnyuk/jsoniter-scala/blob/8f39e1d45fde2a04984498f036cad93286344c30/jsoniter-scala-macros/shared/src/main/scala-3/com/github/plokhotnyuk/jsoniter_scala/macros/JsonCodecMaker.scala#L564-L613 // and around, here sym.primaryConstructor .paramSymss .flatten .map(f => (f, f.tree)) .collect { case (sym, v: ValDef) => (sym, v.tpt.tpe) } } def shortName[T](using Quotes, Type[T]): String = { val fullName = Type.show[T] // attempt at getting a simple name out of fullName (this is likely broken) fullName.takeWhile(_ != '[').split('.').last } inline private def deriveParser[T]: DirectiveHandler[T] = ${ deriveParserImpl[T] } private def deriveParserImpl[T](using q: Quotes, t: Type[T]): Expr[DirectiveHandler[T]] = { import quotes.reflect.* val tSym = TypeTree.of[T].symbol val fields0 = fields[T] val defaultMap: Map[String, Expr[Any]] = { val comp = if (tSym.isClassDef && !tSym.companionClass.isNoSymbol) tSym.companionClass else tSym val bodyOpt = Some(comp) .filter(!_.isNoSymbol) .map(_.tree) .collect { case cd: ClassDef => cd.body } bodyOpt match { case Some(body) => val names = fields0 .map(_._1) .filter(_.flags.is(Flags.HasDefault)) .map(_.name) val values = body.collect { case d @ DefDef(name, _, _, _) if name.startsWith("$lessinit$greater$default") => Ref(d.symbol).asExpr } names.zip(values).toMap case None => Map.empty } } val nameValue = tSym.annotations .find(_.tpe =:= TypeRepr.of[DirectiveGroupName]) .collect { case Apply(_, List(arg)) => arg.asExprOf[String] } .getOrElse { Expr(shortName[T].stripSuffix("Directives")) } val (usageValue, usageMdValue) = tSym.annotations .find(_.tpe =:= TypeRepr.of[DirectiveUsage]) .collect { case Apply(_, List(arg)) => (arg.asExprOf[String], Expr("")) case Apply(_, List(arg, argMd)) => (arg.asExprOf[String], argMd.asExprOf[String]) } .getOrElse { sys.error(s"Missing DirectiveUsage directive on ${Type.show[T]}") } val (descriptionValue, descriptionMdValue) = tSym.annotations .find(_.tpe =:= TypeRepr.of[DirectiveDescription]) .collect { case Apply(_, List(arg)) => (arg.asExprOf[String], Expr("")) case Apply(_, List(arg, argMd)) => (arg.asExprOf[String], argMd.asExprOf[String]) } .getOrElse { sys.error(s"Missing DirectiveDescription directive on ${Type.show[T]}") } val prefixValueOpt = tSym.annotations .find(_.tpe =:= TypeRepr.of[DirectivePrefix]) .collect { case Apply(_, List(arg)) => arg.asExprOf[String] } def withPrefix(name: Expr[String]): Expr[String] = prefixValueOpt match { case None => name case Some(prefixValue) => '{ $prefixValue + $name } } val examplesValue = tSym.annotations .filter(_.tpe =:= TypeRepr.of[DirectiveExamples]) .collect { case Apply(_, List(arg)) => arg.asExprOf[String] } .reverse // not sure in what extent we can rely on the ordering here… val levelValue = tSym.annotations .find(_.tpe =:= TypeRepr.of[DirectiveLevel]) .collect { case Apply(_, List(arg)) => arg.asExprOf[SpecificationLevel] } .getOrElse { sys.error(s"Missing DirectiveLevel directive on ${Type.show[T]}") } def namesFromAnnotations(sym: Symbol) = sym.annotations .filter(_.tpe =:= TypeRepr.of[DirectiveName]) .collect { case Apply(_, List(arg)) => withPrefix(arg.asExprOf[String]) } val keysValue = Expr.ofList { fields0.map { case (sym, _) => Expr.ofList(withPrefix(Expr(sym.name)) +: namesFromAnnotations(sym)) } } val elseCase: ( Expr[ScopedDirective], Expr[Logger] ) => Expr[Either[BuildException, ProcessedDirective[T]]] = (scopedDirective, _) => '{ Left(new UnexpectedDirectiveError($scopedDirective.directive.key)) } val handleValuesImpl = fields0.zipWithIndex.foldRight(elseCase) { case (((sym, tRepr), idx), elseCase0) => val namesFromAnnotations0 = namesFromAnnotations(sym) def typeArgs(tpe: TypeRepr): List[TypeRepr] = tpe match case AppliedType(_, typeArgs) => typeArgs.map(_.dealias) case _ => Nil // from https://github.com/plokhotnyuk/jsoniter-scala/blob/1704a9cbb22b75a59f21ddf2a11427ba24df3212/jsoniter-scala-macros/shared/src/main/scala-3/com/github/plokhotnyuk/jsoniter_scala/macros/JsonCodecMaker.scala#L849-L854 def genNew(argss: List[List[Term]]): Term = val constructorNoTypes = Select(New(Inferred(TypeRepr.of[T])), tSym.primaryConstructor) val constructor = typeArgs(TypeRepr.of[T]) match case Nil => constructorNoTypes case typeArgs => TypeApply(constructorNoTypes, typeArgs.map(Inferred(_))) argss.tail.foldLeft(Apply(constructor, argss.head))((acc, args) => Apply(acc, args)) val newArgs = fields0.map { case (sym, _) => defaultMap.getOrElse(sym.name, sys.error(s"Field ${sym.name} has no default value")) } tRepr.asType match { case '[t] => val parser = Expr.summon[DirectiveValueParser[t]].getOrElse { sys.error(s"Cannot get implicit DirectiveValueParser[${Type.show[t]}]") } val name = withPrefix(Expr(sym.name)) val cond: Expr[String] => Expr[Boolean] = if (namesFromAnnotations0.isEmpty) keyName => '{ DirectiveHandler.normalizeName($keyName) == $name } else { val names = Expr.ofList(name +: namesFromAnnotations0) keyName => '{ $names.contains(DirectiveHandler.normalizeName($keyName)) } } (scopedDirective, logger) => '{ if (${ cond('{ $scopedDirective.directive.key }) }) { val valuesByScope = $scopedDirective.directive.values.groupBy(_.getScope) .toVector .map { case (scopeOrNull, values) => (Option(scopeOrNull), values) } .sortBy(_._1.getOrElse("")) valuesByScope .map { case (scopeOpt, _) => $parser.parse( $scopedDirective.directive.key, $scopedDirective.directive.values, $scopedDirective.cwd, $scopedDirective.maybePath ).map { r => scopeOpt -> ${ genNew(List(newArgs.updated(idx, 'r).map(_.asTerm))) .asExprOf[T] } } } .sequence .left.map(CompositeBuildException(_)) .map { v => val mainOpt = v.collectFirst { case (None, t) => t } val scoped = v.collect { case (Some(scopeStr), t) => // FIXME os.RelPath(…) might fail Scoped( $scopedDirective.cwd / os.RelPath(scopeStr), t ) } ProcessedDirective(mainOpt, scoped) } } else ${ elseCase0(scopedDirective, logger) } } } } '{ new DirectiveHandler[T] { def name = $nameValue def usage = $usageValue override def usageMd = Some($usageMdValue).filter(_.nonEmpty).getOrElse(usage) def description = $descriptionValue override def descriptionMd = Some($descriptionMdValue).filter(_.nonEmpty).getOrElse(description) override def examples = ${ Expr.ofList(examplesValue) } def scalaSpecificationLevel = $levelValue lazy val keys = $keysValue .map { nameAliases => val allAliases = nameAliases.flatMap(key => List( key, DirectiveHandler.pascalCaseSplit(key.toCharArray.toList) .map(_.toLowerCase(Locale.ROOT)) .mkString("-") ) ).distinct Key(allAliases) } def handleValues(scopedDirective: ScopedDirective, logger: Logger) = ${ handleValuesImpl('scopedDirective, 'logger) } } } } inline given derive[T]: DirectiveHandler[T] = DirectiveHandler.deriveParser[T] } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/DirectivePrefix.scala ================================================ package scala.build.directives import scala.annotation.StaticAnnotation final case class DirectivePrefix(prefix: String) extends StaticAnnotation ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/DirectiveUtil.scala ================================================ package scala.build.preprocessing.directives import com.virtuslab.using_directives.custom.model.{BooleanValue, StringValue, Value} import com.virtuslab.using_directives.custom.utils.ast.StringLiteral import dependency.AnyDependency import dependency.parser.DependencyParser import scala.build.Ops.* import scala.build.errors.{BuildException, CompositeBuildException, DependencyFormatError} import scala.build.preprocessing.ScopePath import scala.build.{Position, Positioned} object DirectiveUtil { def isWrappedInDoubleQuotes(v: Value[?]): Boolean = v match { case stringValue: StringValue => stringValue.getRelatedASTNode match { case literal: StringLiteral => literal.getIsWrappedDoubleQuotes() case _ => false } case _ => false } def position(v: Value[?], path: Either[String, os.Path]): Position.File = { val skipQuotes: Boolean = isWrappedInDoubleQuotes(v) val line = v.getRelatedASTNode.getPosition.getLine val column = v.getRelatedASTNode.getPosition.getColumn + (if (skipQuotes) 1 else 0) val endLinePos = column + v.toString.length Position.File(path, (line, column), (line, endLinePos)) } def scope(v: Value[?], cwd: ScopePath): Option[ScopePath] = Option(v.getScope).map((p: String) => cwd / os.RelPath(p)) def concatAllValues( scopedDirective: ScopedDirective ): Seq[String] = scopedDirective.directive.values.collect: case v: StringValue => v.get case v: BooleanValue => v.get.toString def positions(values: Seq[Value[?]], path: Either[String, os.Path]): Seq[Position] = values.map { v => val line = v.getRelatedASTNode.getPosition.getLine val column = v.getRelatedASTNode.getPosition.getColumn Position.File(path, (line, column), (line, column)) } extension (deps: List[Positioned[String]]) { def asDependencies: Either[BuildException, Seq[Positioned[AnyDependency]]] = deps .map { positionedDep => positionedDep.map { str => DependencyParser.parse(str).left.map { error => new DependencyFormatError( str, error, positions = positionedDep.positions ) } }.eitherSequence } .sequence .left.map(CompositeBuildException(_)) } } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/Exclude.scala ================================================ package scala.build.preprocessing.directives import scala.build.EitherCps.either import scala.build.Ops.* import scala.build.Positioned import scala.build.directives.* import scala.build.errors.BuildException import scala.build.options.{BuildOptions, InternalOptions} import scala.cli.commands.SpecificationLevel @DirectiveGroupName("Exclude sources") @DirectiveExamples("//> using exclude utils.scala") @DirectiveExamples("//> using exclude examples/* */resources/*") @DirectiveExamples("//> using exclude *.sc") @DirectiveUsage( "`//> using exclude `_pattern_ | `//> using exclude `_pattern_ _pattern_ …", """`//> using exclude` _pattern_ | |`//> using exclude` _pattern1_ _pattern2_ … |""".stripMargin ) @DirectiveDescription("Exclude sources from the project") @DirectiveLevel(SpecificationLevel.SHOULD) final case class Exclude(exclude: List[Positioned[String]] = Nil) extends HasBuildOptions { def buildOptions: Either[BuildException, BuildOptions] = either { BuildOptions( internal = InternalOptions( exclude = exclude ) ) } } object Exclude { val handler: DirectiveHandler[Exclude] = DirectiveHandler.derive } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/JavaHome.scala ================================================ package scala.build.preprocessing.directives import scala.build.EitherCps.{either, value} import scala.build.Positioned import scala.build.directives.* import scala.build.errors.{BuildException, WrongJavaHomePathError} import scala.build.options.{BuildOptions, JavaOptions} import scala.cli.commands.SpecificationLevel import scala.util.Try @DirectiveGroupName("Java home") @DirectiveExamples("//> using javaHome /Users/Me/jdks/11") @DirectiveUsage( "//> using javaHome _path_", "`//> using javaHome` _path_" ) @DirectiveDescription("Sets Java home used to run your application or tests") @DirectiveLevel(SpecificationLevel.SHOULD) final case class JavaHome( javaHome: DirectiveValueParser.WithScopePath[Option[Positioned[String]]] = DirectiveValueParser.WithScopePath.empty(None) ) extends HasBuildOptions { def buildOptions: Either[BuildException, BuildOptions] = either { javaHome.value match { case None => BuildOptions() case Some(homePosStr) => val root = value(Directive.osRoot(javaHome.scopePath, homePosStr.positions.headOption)) val home = value { homePosStr .map { homeStr => Try(os.Path(homeStr, root)).toEither.left.map { ex => new WrongJavaHomePathError(homeStr, ex) } } .eitherSequence } BuildOptions( javaOptions = JavaOptions( javaHomeOpt = Some(home) ) ) } } } object JavaHome { val handler: DirectiveHandler[JavaHome] = DirectiveHandler.derive } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/JavaOptions.scala ================================================ package scala.build.preprocessing.directives import scala.build.directives.* import scala.build.errors.BuildException import scala.build.options.WithBuildRequirements.* import scala.build.options.{BuildOptions, JavaOpt, Scope, ShadowingSeq, WithBuildRequirements} import scala.build.{Positioned, options} import scala.cli.commands.SpecificationLevel @DirectiveGroupName("Java options") @DirectiveExamples("//> using javaOpt -Xmx2g -Dsomething=a") @DirectiveExamples("//> using test.javaOpt -Dsomething=a") @DirectiveUsage( "//> using javaOpt _options_", """`//> using javaOpt` _options_ |`//> using javaOptions` _options_` | |`//> using test.javaOpt` _options_ |`//> using test.javaOptions` _options_` |""".stripMargin ) @DirectiveDescription("Add Java options which will be passed when running an application.") @DirectiveLevel(SpecificationLevel.MUST) final case class JavaOptions( @DirectiveName("javaOpt") javaOptions: List[Positioned[String]] = Nil, @DirectiveName("test.javaOptions") @DirectiveName("test.javaOpt") testJavaOptions: List[Positioned[String]] = Nil ) extends HasBuildOptionsWithRequirements { def buildOptionsList: List[Either[BuildException, WithBuildRequirements[BuildOptions]]] = List( JavaOptions.buildOptions(javaOptions).map(_.withEmptyRequirements), JavaOptions.buildOptions(testJavaOptions).map(_.withScopeRequirement(Scope.Test)) ) } object JavaOptions { val handler: DirectiveHandler[JavaOptions] = DirectiveHandler.derive def buildOptions(javaOptions: List[Positioned[String]]): Either[BuildException, BuildOptions] = Right { BuildOptions(javaOptions = options.JavaOptions(javaOpts = ShadowingSeq.from(javaOptions.map(_.map(JavaOpt(_))))) ) } } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/JavaProps.scala ================================================ package scala.build.preprocessing.directives import scala.build.directives.* import scala.build.errors.BuildException import scala.build.options.WithBuildRequirements.* import scala.build.options.{BuildOptions, JavaOpt, Scope, ShadowingSeq, WithBuildRequirements} import scala.build.{Positioned, options} import scala.cli.commands.SpecificationLevel @DirectiveGroupName("Java properties") @DirectiveExamples("//> using javaProp foo1=bar foo2") @DirectiveExamples("//> using test.javaProp foo3=bar foo4") @DirectiveUsage( "//> using javaProp _key=val_", """`//> using javaProp` _key=value_ | |`//> using javaProp` _key_ | |`//> using test.javaProp` _key=value_ | |`//> using test.javaProp` _key_ |""".stripMargin ) @DirectiveDescription("Add Java properties") @DirectiveLevel(SpecificationLevel.MUST) final case class JavaProps( @DirectiveName("javaProp") javaProperty: List[Positioned[String]] = Nil, @DirectiveName("test.javaProperty") @DirectiveName("test.javaProp") testJavaProperty: List[Positioned[String]] = Nil ) extends HasBuildOptionsWithRequirements { def buildOptionsList: List[Either[BuildException, WithBuildRequirements[BuildOptions]]] = List( JavaProps.buildOptions(javaProperty).map(_.withEmptyRequirements), JavaProps.buildOptions(testJavaProperty).map(_.withScopeRequirement(Scope.Test)) ) } object JavaProps { val handler: DirectiveHandler[JavaProps] = DirectiveHandler.derive def buildOptions(javaProperties: List[Positioned[String]]): Either[BuildException, BuildOptions] = Right { val javaOpts: Seq[Positioned[JavaOpt]] = javaProperties.map { positioned => positioned.map { v => v.split("=") match { case Array(k) => JavaOpt(s"-D$k") case Array(k, v) => JavaOpt(s"-D$k=$v") } } } BuildOptions( javaOptions = options.JavaOptions( javaOpts = ShadowingSeq.from(javaOpts) ) ) } } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/JavacOptions.scala ================================================ package scala.build.preprocessing.directives import scala.build.directives.* import scala.build.errors.BuildException import scala.build.options.WithBuildRequirements.* import scala.build.options.{BuildOptions, Scope, WithBuildRequirements} import scala.build.{Positioned, options} import scala.cli.commands.SpecificationLevel @DirectiveGroupName("Javac options") @DirectiveExamples("//> using javacOpt -source 1.8 -target 1.8") @DirectiveExamples("//> using test.javacOpt -source 1.8 -target 1.8") @DirectiveUsage( "//> using javacOpt _options_", """`//> using javacOpt` _options_ | |`//> using javacOptions` _options_ | |`//> using test.javacOpt` _options_ | |`//> using test.javacOptions` _options_ |""".stripMargin ) @DirectiveDescription("Add Javac options which will be passed when compiling sources.") @DirectiveLevel(SpecificationLevel.SHOULD) final case class JavacOptions( @DirectiveName("javacOpt") javacOptions: List[Positioned[String]] = Nil, @DirectiveName("test.javacOptions") @DirectiveName("test.javacOpt") testJavacOptions: List[Positioned[String]] = Nil ) extends HasBuildOptionsWithRequirements { def buildOptionsList: List[Either[BuildException, WithBuildRequirements[BuildOptions]]] = List( JavacOptions.buildOptions(javacOptions).map(_.withEmptyRequirements), JavacOptions.buildOptions(testJavacOptions).map(_.withScopeRequirement(Scope.Test)) ) } object JavacOptions { val handler: DirectiveHandler[JavacOptions] = DirectiveHandler.derive def buildOptions(javacOptions: List[Positioned[String]]): Either[BuildException, BuildOptions] = Right(BuildOptions(javaOptions = options.JavaOptions(javacOptions = javacOptions))) } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/Jvm.scala ================================================ package scala.build.preprocessing.directives import scala.build.directives.* import scala.build.errors.BuildException import scala.build.options.BuildOptions import scala.build.{Positioned, options} import scala.cli.commands.SpecificationLevel @DirectiveGroupName("JVM version") @DirectiveExamples("//> using jvm 11") @DirectiveExamples("//> using jvm temurin:11") @DirectiveExamples("//> using jvm graalvm:21") @DirectiveUsage( "//> using jvm _value_", "`//> using jvm` _value_" ) @DirectiveDescription( "Use a specific JVM, such as `14`, `temurin:11`, or `graalvm:21`, or `system`. " + "scala-cli uses [coursier](https://get-coursier.io/) to fetch JVMs, so you can use `cs java --available` to list the available JVMs." ) @DirectiveLevel(SpecificationLevel.SHOULD) final case class Jvm(jvm: Option[Positioned[String]] = None) extends HasBuildOptions { def buildOptions: Either[BuildException, BuildOptions] = { val buildOpt = BuildOptions( javaOptions = options.JavaOptions( jvmIdOpt = jvm ) ) Right(buildOpt) } } object Jvm { val handler: DirectiveHandler[Jvm] = DirectiveHandler.derive } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/MainClass.scala ================================================ package scala.build.preprocessing.directives import scala.build.directives.* import scala.build.errors.BuildException import scala.build.options.BuildOptions import scala.cli.commands.SpecificationLevel @DirectiveGroupName("Main class") @DirectiveExamples("//> using mainClass HelloWorld") @DirectiveUsage( "//> using mainClass _main-class_", "`//> using mainClass` _main-class_" ) @DirectiveDescription("Specify default main class") @DirectiveLevel(SpecificationLevel.MUST) final case class MainClass(mainClass: Option[String] = None) extends HasBuildOptions { def buildOptions: Either[BuildException, BuildOptions] = Right(BuildOptions(mainClass = mainClass)) } object MainClass { val handler: DirectiveHandler[MainClass] = DirectiveHandler.derive } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/ObjectWrapper.scala ================================================ package scala.build.preprocessing.directives import scala.build.directives.* import scala.build.errors.BuildException import scala.build.options.{BuildOptions, ScriptOptions} import scala.cli.commands.SpecificationLevel @DirectiveExamples("//> using objectWrapper") @DirectiveUsage("//> using objectWrapper", "`//> using objectWrapper`") @DirectiveDescription("Set the default code wrapper for scripts to object wrapper") @DirectiveLevel(SpecificationLevel.EXPERIMENTAL) final case class ObjectWrapper( @DirectiveName("object.wrapper") @DirectiveName("wrapper.object") objectWrapper: Boolean = false ) extends HasBuildOptions { def buildOptions: Either[BuildException, BuildOptions] = val options = BuildOptions(scriptOptions = ScriptOptions(forceObjectWrapper = Some(true)) ) Right(options) } object ObjectWrapper { val handler: DirectiveHandler[ObjectWrapper] = DirectiveHandler.derive } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/Packaging.scala ================================================ package scala.build.preprocessing.directives import dependency.parser.ModuleParser import scala.build.EitherCps.{either, value} import scala.build.Ops.* import scala.build.Positioned import scala.build.directives.* import scala.build.errors.{ BuildException, CompositeBuildException, MalformedInputError, ModuleFormatError, WrongDirectoryPathError } import scala.build.options.* import scala.build.options.packaging.{DockerOptions, NativeImageOptions} import scala.cli.commands.SpecificationLevel import scala.util.Try @DirectiveGroupName("Packaging") @DirectivePrefix("packaging.") @DirectiveExamples("//> using packaging.packageType assembly") @DirectiveExamples("//> using packaging.output foo") @DirectiveExamples("//> using packaging.provided org.apache.spark::spark-sql") @DirectiveExamples("//> using packaging.graalvmArgs --no-fallback") @DirectiveExamples("//> using packaging.dockerFrom openjdk:11") @DirectiveExamples("//> using packaging.dockerImageTag 1.0.0") @DirectiveExamples("//> using packaging.dockerImageRegistry virtuslab") @DirectiveExamples("//> using packaging.dockerImageRepository scala-cli") @DirectiveExamples("//> using packaging.dockerCmd sh") @DirectiveExamples("//> using packaging.dockerCmd node") @DirectiveExamples( "//> using packaging.dockerExtraDirectories path/to/directory1 path/to/directory2" ) @DirectiveExamples("//> using packaging.dockerExtraDirectory path/to/directory") @DirectiveUsage( """using packaging.packageType [package type] |using packaging.output [destination path] |using packaging.provided [module] |using packaging.graalvmArgs [args] |using packaging.dockerFrom [base docker image] |using packaging.dockerImageTag [image tag] |using packaging.dockerImageRegistry [image registry] |using packaging.dockerImageRepository [image repository] |using packaging.dockerCmd [docker command] |using packaging.dockerExtraDirectories [directories] |""".stripMargin, """`//> using packaging.packageType` _package-type_ | |`//> using packaging.output` _destination-path_ | |`//> using packaging.provided` _module_ | |`//> using packaging.graalvmArgs` _args_ | |`//> using packaging.dockerFrom` _base-docker-image_ | |`//> using packaging.dockerImageTag` _image-tag_ | |`//> using packaging.dockerImageRegistry` _image-registry_ | |`//> using packaging.dockerImageRepository` _image-repository_ | |`//> using packaging.dockerCmd` _docker-command_ | |`//> using packaging.dockerExtraDirectories` _directories_ |`//> using packaging.dockerExtraDirectory` _directory_ | |""".stripMargin ) @DirectiveDescription("Set parameters for packaging") @DirectiveLevel(SpecificationLevel.RESTRICTED) final case class Packaging( packageType: Option[Positioned[String]] = None, output: Option[String] = None, provided: List[Positioned[String]] = Nil, graalvmArgs: List[Positioned[String]] = Nil, dockerFrom: Option[String] = None, dockerImageTag: Option[String] = None, dockerImageRegistry: Option[String] = None, dockerImageRepository: Option[String] = None, dockerCmd: Option[String] = None, @DirectiveName("dockerExtraDirectory") dockerExtraDirectories: DirectiveValueParser.WithScopePath[List[Positioned[String]]] = DirectiveValueParser.WithScopePath.empty(Nil) ) extends HasBuildOptions { def buildOptions: Either[BuildException, BuildOptions] = either { val maybePackageTypeOpt = packageType .map { input => PackageType.parse(input.value).toRight { new MalformedInputError( "package-type", input.value, PackageType.mapping.map(_._1).mkString("|"), positions = input.positions ) } } .sequence val maybeOutput = output .map { path => try Right(os.Path(path, os.pwd)) // !!! catch { case _: IllegalArgumentException => Left(???) } } .sequence val maybeProvided = provided .map { input => ModuleParser.parse(input.value) .left.map { err => new ModuleFormatError(input.value, err, positions = input.positions) } } .sequence .left.map(CompositeBuildException(_)) val (packageTypeOpt, output0, provided0) = value { (maybePackageTypeOpt, maybeOutput, maybeProvided) .traverseN .left.map(CompositeBuildException(_)) } val cwd = dockerExtraDirectories.scopePath val extraDirectories = value { dockerExtraDirectories .value .map { posPathStr => val eitherRootPathOrBuildException = Directive.osRoot(cwd, posPathStr.positions.headOption) eitherRootPathOrBuildException.flatMap { root => Try(os.Path(posPathStr.value, root)) .toEither .left.map(new WrongDirectoryPathError(_)) } } .sequence .left.map(CompositeBuildException(_)) } BuildOptions( internal = InternalOptions( keepResolution = provided0.nonEmpty || packageTypeOpt.contains(PackageType.Spark) ), notForBloopOptions = PostBuildOptions( packageOptions = PackageOptions( packageTypeOpt = packageTypeOpt, output = output0.map(_.toString), provided = provided0, dockerOptions = DockerOptions( from = dockerFrom, imageRegistry = dockerImageRegistry, imageRepository = dockerImageRepository, imageTag = dockerImageTag, cmd = dockerCmd, extraDirectories = extraDirectories ), nativeImageOptions = NativeImageOptions( graalvmArgs = graalvmArgs ) ) ) ) } } object Packaging { val handler: DirectiveHandler[Packaging] = DirectiveHandler.derive } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/Platform.scala ================================================ package scala.build.preprocessing.directives import scala.build.EitherCps.{either, value} import scala.build.Ops.* import scala.build.directives.* import scala.build.errors.{ BuildException, CompositeBuildException, MalformedPlatformError, UnexpectedJvmPlatformVersionError } import scala.build.options.{ BuildOptions, ConfigMonoid, ScalaJsOptions, ScalaNativeOptions, ScalaOptions } import scala.build.{Positioned, options} import scala.cli.commands.SpecificationLevel @DirectiveGroupName("Platform") @DirectiveExamples("//> using platform scala-js") @DirectiveExamples("//> using platforms jvm scala-native") @DirectiveUsage( "//> using platform (jvm|scala-js|js|scala-native|native)+", """`//> using platform` (`jvm`|`scala-js`|`js`|`scala-native`|`native`)+ | |`//> using platforms` (`jvm`|`scala-js`|`js`|`scala-native`|`native`)+ |""".stripMargin ) @DirectiveDescription("Set the default platform to Scala.js or Scala Native") @DirectiveLevel(SpecificationLevel.SHOULD) final case class Platform( @DirectiveName("platform") platforms: List[Positioned[String]] = Nil ) extends HasBuildOptions { private def split(input: String): (String, Option[String]) = { val idx = input.indexOf(':') if (idx < 0) (input, None) else (input.take(idx), Some(input.drop(idx + 1))) } def buildOptions: Either[BuildException, BuildOptions] = either { val allBuildOptions = value { platforms .map { input => val (pfStr, pfVerOpt) = split(input.value) options.Platform.parse(options.Platform.normalize(pfStr)) match { case None => Left(new MalformedPlatformError(pfStr, positions = input.positions)) case Some(pf) => (pf, pfVerOpt) match { case (_, None) => Right( BuildOptions( scalaOptions = ScalaOptions( platform = Some(input.map(_ => pf)) ) ) ) case (options.Platform.JVM, Some(ver)) => Left(new UnexpectedJvmPlatformVersionError(ver, input.positions)) case (options.Platform.JS, Some(ver)) => Right( BuildOptions( scalaOptions = ScalaOptions( platform = Some(input.map(_ => pf)) ), scalaJsOptions = ScalaJsOptions( version = Some(ver) ) ) ) case (options.Platform.Native, Some(ver)) => Right( BuildOptions( scalaOptions = ScalaOptions( platform = Some(input.map(_ => pf)) ), scalaNativeOptions = ScalaNativeOptions( version = Some(ver) ) ) ) } } } .sequence .left.map(CompositeBuildException(_)) } allBuildOptions.headOption.fold(BuildOptions()) { _ => val mergedBuildOptions = allBuildOptions.foldLeft(BuildOptions())(_.orElse(_)) val mainPlatformOpt = mergedBuildOptions.scalaOptions.platform.map(_.value) // shouldn't be empty… val extraPlatforms = ConfigMonoid.sum { allBuildOptions .flatMap(_.scalaOptions.platform.toSeq) .filter(p => !mainPlatformOpt.contains(p.value)) .map(p => Map(p.value -> p.map(_ => ()))) } mergedBuildOptions.copy( scalaOptions = mergedBuildOptions.scalaOptions.copy( extraPlatforms = mergedBuildOptions.scalaOptions.extraPlatforms ++ extraPlatforms ) ) } } } object Platform { val handler: DirectiveHandler[Platform] = DirectiveHandler.derive } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/Plugin.scala ================================================ package scala.build.preprocessing.directives import dependency.AnyDependency import scala.build.EitherCps.{either, value} import scala.build.Positioned import scala.build.directives.* import scala.build.errors.BuildException import scala.build.options.{BuildOptions, ScalaOptions} import scala.build.preprocessing.directives.DirectiveUtil.* import scala.cli.commands.SpecificationLevel @DirectiveGroupName("Compiler plugins") @DirectiveExamples("//> using plugin org.typelevel:::kind-projector:0.13.4") @DirectiveUsage( "//> using plugin org:name:ver | //> using plugins org:name:ver org2:name2:ver2", "`using plugin` _org_`:`_name_`:`_ver_" ) @DirectiveDescription("Adds compiler plugins") @DirectiveLevel(SpecificationLevel.MUST) final case class Plugin( @DirectiveName("plugins") plugin: List[Positioned[String]] = Nil ) extends HasBuildOptions { def buildOptions: Either[BuildException, BuildOptions] = either { val dependencies: Seq[Positioned[AnyDependency]] = value(plugin.asDependencies) BuildOptions(scalaOptions = ScalaOptions(compilerPlugins = dependencies)) } } object Plugin { val handler: DirectiveHandler[Plugin] = DirectiveHandler.derive } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/ProcessedDirective.scala ================================================ package scala.build.preprocessing.directives import scala.build.Ops.* import scala.build.errors.{BuildException, CompositeBuildException} import scala.build.preprocessing.Scoped final case class ProcessedDirective[+T](global: Option[T], scoped: Seq[Scoped[T]]) { def map[U](f: T => U): ProcessedDirective[U] = ProcessedDirective(global.map(f), scoped.map(_.map(f))) def mapE[U](f: T => Either[BuildException, U]): Either[BuildException, ProcessedDirective[U]] = { val maybeGlobal = global.map(f) match { case None => Right(None) case Some(Left(e)) => Left(e) case Some(Right(u)) => Right(Some(u)) } val maybeScoped = scoped.map(_.mapE(f)).sequence.left.map(CompositeBuildException(_)) (maybeGlobal, maybeScoped) .traverseN .left.map(CompositeBuildException(_)) .map { case (global0, scoped0) => ProcessedDirective(global0, scoped0) } } } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/Publish.scala ================================================ package scala.build.preprocessing.directives import scala.build.EitherCps.{either, value} import scala.build.Ops.* import scala.build.Positioned import scala.build.directives.* import scala.build.errors.{BuildException, CompositeBuildException} import scala.build.options.publish.{Developer, License, Vcs} import scala.build.options.{BuildOptions, PostBuildOptions, PublishOptions} import scala.cli.commands.SpecificationLevel @DirectiveGroupName("Publish") @DirectivePrefix("publish.") @DirectiveExamples("//> using publish.organization io.github.myself") @DirectiveExamples("//> using publish.name my-library") @DirectiveExamples("//> using publish.moduleName scala-cli_3") @DirectiveExamples("//> using publish.version 0.1.1") @DirectiveExamples("//> using publish.url https://github.com/VirtusLab/scala-cli") @DirectiveExamples("//> using publish.license MIT") @DirectiveExamples("//> using publish.vcs https://github.com/VirtusLab/scala-cli.git") @DirectiveExamples("//> using publish.vcs github:VirtusLab/scala-cli") @DirectiveExamples("//> using publish.description \"Lorem ipsum dolor sit amet\"") @DirectiveExamples("//> using publish.developer alexme|Alex Me|https://alex.me") @DirectiveExamples( "//> using publish.developers alexme|Alex Me|https://alex.me Gedochao|Gedo Chao|https://github.com/Gedochao" ) @DirectiveExamples("//> using publish.scalaVersionSuffix _2.13") @DirectiveExamples("//> using publish.scalaVersionSuffix _3") @DirectiveExamples("//> using publish.scalaPlatformSuffix _sjs1") @DirectiveExamples("//> using publish.scalaPlatformSuffix _native0.4") @DirectiveUsage( "//> using publish.[key] [value]", """`//> using publish.organization` value | |`//> using publish.name` value | |`//> using publish.moduleName` value | |`//> using publish.version` value | |`//> using publish.url` value | |`//> using publish.license` value | |`//> using publish.vcs` value | |`//> using publish.scm` value | |`//> using publish.versionControl` value | |`//> using publish.description` value | |`//> using publish.developer` value | |`//> using publish.developers` value1 value2 | |`//> using publish.scalaVersionSuffix` value | |`//> using publish.scalaPlatformSuffix` value | |""".stripMargin ) @DirectiveDescription("Set parameters for publishing") @DirectiveLevel(SpecificationLevel.EXPERIMENTAL) final case class Publish( organization: Option[Positioned[String]] = None, name: Option[Positioned[String]] = None, moduleName: Option[Positioned[String]] = None, version: Option[Positioned[String]] = None, url: Option[Positioned[String]] = None, license: Option[Positioned[String]] = None, @DirectiveName("scm") @DirectiveName("versionControl") vcs: Option[Positioned[String]] = None, description: Option[String] = None, @DirectiveName("developer") developers: List[Positioned[String]] = Nil, scalaVersionSuffix: Option[String] = None, scalaPlatformSuffix: Option[String] = None ) extends HasBuildOptions { def buildOptions: Either[BuildException, BuildOptions] = either { val maybeLicense = license .map(License.parse) .sequence val maybeVcs = vcs .map(Vcs.parse) .sequence val maybeDevelopers = developers .map(Developer.parse) .sequence .left.map(CompositeBuildException(_)) val (licenseOpt, vcsOpt, developers0) = value { (maybeLicense, maybeVcs, maybeDevelopers) .traverseN .left.map(CompositeBuildException(_)) } val publishOptions = PublishOptions( organization = organization, name = name, moduleName = moduleName, version = version, url = url, license = licenseOpt, versionControl = vcsOpt, description = description, developers = developers0, scalaVersionSuffix = scalaVersionSuffix, scalaPlatformSuffix = scalaPlatformSuffix ) BuildOptions( notForBloopOptions = PostBuildOptions( publishOptions = publishOptions ) ) } } object Publish { val handler: DirectiveHandler[Publish] = DirectiveHandler.derive } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/PublishContextual.scala ================================================ package scala.build.preprocessing.directives import scala.build.EitherCps.{either, value} import scala.build.Ops.* import scala.build.Positioned import scala.build.directives.* import scala.build.errors.{BuildException, CompositeBuildException, MalformedInputError} import scala.build.options.* import scala.build.options.publish.ConfigPasswordOption import scala.cli.commands.SpecificationLevel import scala.cli.signing.shared.PasswordOption trait PublishContextual { def computeVersion: Option[Positioned[String]] def repository: Option[String] def gpgKey: Option[String] def gpgOptions: List[String] def secretKey: Option[Positioned[String]] def secretKeyPassword: Option[Positioned[String]] def publicKey: Option[Positioned[String]] def user: Option[Positioned[String]] def password: Option[Positioned[String]] def realm: Option[String] def doc: Option[Boolean] def buildOptions(isCi: Boolean): Either[BuildException, BuildOptions] = either { val maybeComputeVersion = computeVersion .map(ComputeVersion.parse) .sequence val maybeSecretKey = secretKey .map { input => PublishContextual.parsePasswordOption(input) .map(ConfigPasswordOption.ActualOption(_)) } .sequence val maybeSecretKeyPassword = secretKeyPassword .map { input => PublishContextual.parsePasswordOption(input) .map(ConfigPasswordOption.ActualOption(_)) } .sequence val maybePublicKey = publicKey .map { input => PublishContextual.parsePasswordOption(input) .map(ConfigPasswordOption.ActualOption(_)) } .sequence val maybeUser = user .map(PublishContextual.parsePasswordOption) .sequence val maybePassword = password .map(PublishContextual.parsePasswordOption) .sequence val ( computeVersionOpt, secretKeyOpt, secretKeyPasswordOpt, publicKeyOpt, userOpt, passwordOpt ) = value { ( maybeComputeVersion, maybeSecretKey, maybeSecretKeyPassword, maybePublicKey, maybeUser, maybePassword ) .traverseN .left.map(CompositeBuildException(_)) } val publishContextualOptions = PublishContextualOptions( computeVersion = computeVersionOpt, repository = repository, docJar = doc, gpgSignatureId = gpgKey, gpgOptions = gpgOptions, secretKey = secretKeyOpt, secretKeyPassword = secretKeyPasswordOpt, publicKey = publicKeyOpt, repoUser = userOpt, repoPassword = passwordOpt, repoRealm = realm ) val publishOptions = if (isCi) PublishOptions(ci = publishContextualOptions) else PublishOptions(local = publishContextualOptions) BuildOptions( notForBloopOptions = PostBuildOptions( publishOptions = publishOptions ) ) } } object PublishContextual { @DirectiveGroupName("Publish (contextual)") @DirectivePrefix("publish.") @DirectiveExamples("//> using publish.computeVersion git:tag") @DirectiveExamples("//> using publish.repository central-s01") @DirectiveExamples("//> using publish.secretKey env:PUBLISH_SECRET_KEY") @DirectiveExamples("//> using publish.doc false") @DirectiveUsage( "//> using publish.(computeVersion|repository|secretKey|…) [value]", """`//> using publish.computeVersion` value | |`//> using publish.repository` value | |`//> using publish.secretKey` value | |`//> using publish.doc` boolean | |""".stripMargin ) @DirectiveDescription("Set contextual parameters for publishing") @DirectiveLevel(SpecificationLevel.EXPERIMENTAL) // format: off final case class Local( computeVersion: Option[Positioned[String]] = None, repository: Option[String] = None, gpgKey: Option[String] = None, gpgOptions: List[String] = Nil, secretKey: Option[Positioned[String]] = None, secretKeyPassword: Option[Positioned[String]] = None, publicKey: Option[Positioned[String]] = None, user: Option[Positioned[String]] = None, password: Option[Positioned[String]] = None, realm: Option[String] = None, doc: Option[Boolean] = None ) extends HasBuildOptions with PublishContextual { // format: on def buildOptions: Either[BuildException, BuildOptions] = buildOptions(isCi = false) } object Local { val handler: DirectiveHandler[Local] = DirectiveHandler.derive } @DirectiveGroupName("Publish (CI)") @DirectivePrefix("publish.ci.") @DirectiveExamples("//> using publish.ci.computeVersion git:tag") @DirectiveExamples("//> using publish.ci.repository central-s01") @DirectiveExamples("//> using publish.ci.secretKey env:PUBLISH_SECRET_KEY") @DirectiveUsage( "//> using publish.[.ci](computeVersion|repository|secretKey|…) [value]", """`//> using publish.ci.computeVersion` value | |`//> using publish.ci.repository` value | |`//> using publish.ci.secretKey` value | |""".stripMargin ) @DirectiveDescription("Set CI parameters for publishing") @DirectiveLevel(SpecificationLevel.RESTRICTED) // format: off final case class CI( computeVersion: Option[Positioned[String]] = None, repository: Option[String] = None, gpgKey: Option[String] = None, gpgOptions: List[String] = Nil, secretKey: Option[Positioned[String]] = None, secretKeyPassword: Option[Positioned[String]] = None, publicKey: Option[Positioned[String]] = None, user: Option[Positioned[String]] = None, password: Option[Positioned[String]] = None, realm: Option[String] = None, doc: Option[Boolean] = None ) extends HasBuildOptions with PublishContextual { // format: on def buildOptions: Either[BuildException, BuildOptions] = buildOptions(isCi = true) } object CI { val handler: DirectiveHandler[CI] = DirectiveHandler.derive } private def parsePasswordOption(input: Positioned[String]) : Either[BuildException, PasswordOption] = PasswordOption.parse(input.value).left.map { _ => new MalformedInputError( "secret", input.value, "file:_path_|value:_value_|env:_env_var_name_", positions = input.positions ) } } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/Python.scala ================================================ package scala.build.preprocessing.directives import scala.build.directives.* import scala.build.errors.BuildException import scala.build.internal.Constants import scala.build.options.{BuildOptions, PostBuildOptions, ScalaNativeOptions} import scala.cli.commands.SpecificationLevel @DirectiveExamples("//> using python") @DirectiveUsage("//> using python", "`//> using python`") @DirectiveDescription("Enable Python support") @DirectiveLevel(SpecificationLevel.EXPERIMENTAL) final case class Python( python: Boolean = false ) extends HasBuildOptions { def buildOptions: Either[BuildException, BuildOptions] = { val options = BuildOptions( notForBloopOptions = PostBuildOptions( python = Some(true) ), scalaNativeOptions = ScalaNativeOptions( maxDefaultNativeVersions = List(Constants.scalaPyMaxScalaNative -> Python.maxScalaNativeWarningMsg) ) ) Right(options) } } object Python { val handler: DirectiveHandler[Python] = DirectiveHandler.derive val maxScalaNativeWarningMsg = s"ScalaPy does not support Scala Native ${Constants.scalaNativeVersion}, ${Constants.scalaPyMaxScalaNative} should be used instead." } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/Repository.scala ================================================ package scala.build.preprocessing.directives import scala.build.directives.* import scala.build.errors.BuildException import scala.build.options.{BuildOptions, ClassPathOptions} import scala.cli.commands.SpecificationLevel @DirectiveGroupName("Repository") @DirectiveExamples("//> using repository jitpack") @DirectiveExamples("//> using repository sonatype:snapshots") @DirectiveExamples("//> using repository ivy2Local") @DirectiveExamples("//> using repository m2Local") @DirectiveExamples( "//> using repository https://maven-central.storage-download.googleapis.com/maven2" ) @DirectiveUsage( "//> using repository _repository_", "`//> using repository` _repository_" ) @DirectiveDescription(Repository.usageMsg) @DirectiveLevel(SpecificationLevel.SHOULD) final case class Repository( @DirectiveName("repository") repositories: List[String] = Nil ) extends HasBuildOptions { def buildOptions: Either[BuildException, BuildOptions] = { val buildOpt = BuildOptions( classPathOptions = ClassPathOptions( extraRepositories = repositories ) ) Right(buildOpt) } } object Repository { val handler: DirectiveHandler[Repository] = DirectiveHandler.derive val usageMsg = """Add repositories for dependency resolution. | |Accepts predefined repositories supported by Coursier (like `sonatype:snapshots`, `ivy2Local` or `m2Local`) or a URL of the root of Maven repository""".stripMargin } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/RequirePlatform.scala ================================================ package scala.build.preprocessing.directives import scala.build.EitherCps.{either, value} import scala.build.directives.* import scala.build.errors.{BuildException, MalformedPlatformError} import scala.build.options.{BuildRequirements, Platform} import scala.build.{Positioned, options} import scala.cli.commands.SpecificationLevel @DirectiveGroupName("Platform") @DirectivePrefix("target.") @DirectiveDescription("Require a Scala platform for the current file") @DirectiveExamples("//> using target.platform scala-js") @DirectiveExamples("//> using target.platform scala-js scala-native") @DirectiveExamples("//> using target.platform jvm") @DirectiveUsage( "//> using target.platform _platform_", "`//> using target.platform` _platform_" ) @DirectiveLevel(SpecificationLevel.EXPERIMENTAL) final case class RequirePlatform( @DirectiveName("platform") platforms: List[Positioned[String]] = Nil ) extends HasBuildRequirements { def buildRequirements: Either[BuildException, BuildRequirements] = either { val platformSet = value { Platform.parseSpec(platforms.map(_.value).map(options.Platform.normalize)).toRight { new MalformedPlatformError( platforms.map(_.value).mkString(", "), Positioned.sequence(platforms).positions ) } } BuildRequirements( platform = Seq(BuildRequirements.PlatformRequirement(platformSet)) ) } } object RequirePlatform { val handler: DirectiveHandler[RequirePlatform] = DirectiveHandler.derive } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/RequireScalaVersion.scala ================================================ package scala.build.preprocessing.directives import scala.build.directives.* import scala.build.errors.BuildException import scala.build.options.BuildRequirements import scala.cli.commands.SpecificationLevel @DirectiveGroupName("Scala version") @DirectivePrefix("target.") @DirectiveDescription("Require a Scala version for the current file") @DirectiveExamples("//> using target.scala 3") @DirectiveUsage( "//> using target.scala _version_", "`//> using target.scala` _version_" ) @DirectiveLevel(SpecificationLevel.EXPERIMENTAL) final case class RequireScalaVersion( scala: Option[DirectiveValueParser.MaybeNumericalString] = None ) extends HasBuildRequirements { def buildRequirements: Either[BuildException, BuildRequirements] = { val requirements = BuildRequirements( scalaVersion = scala.toSeq.map { ns => BuildRequirements.VersionEquals(ns.value, loose = true) } ) Right(requirements) } } object RequireScalaVersion { val handler: DirectiveHandler[RequireScalaVersion] = DirectiveHandler.derive } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/RequireScalaVersionBounds.scala ================================================ package scala.build.preprocessing.directives import scala.build.directives.* import scala.build.errors.BuildException import scala.build.options.BuildRequirements import scala.cli.commands.SpecificationLevel @DirectiveGroupName("Scala version bounds") @DirectivePrefix("target.scala.") @DirectiveDescription("Require a Scala version for the current file") @DirectiveExamples("//> using target.scala.>= 2.13") @DirectiveExamples("//> using target.scala.< 3.0.2") @DirectiveUsage( "//> using target.scala.>= _version_", "`//> using target.scala.>=` _version_" ) @DirectiveLevel(SpecificationLevel.EXPERIMENTAL) final case class RequireScalaVersionBounds( `==`: Option[DirectiveValueParser.MaybeNumericalString] = None, `>=`: Option[DirectiveValueParser.MaybeNumericalString] = None, `<=`: Option[DirectiveValueParser.MaybeNumericalString] = None, `>`: Option[DirectiveValueParser.MaybeNumericalString] = None, `<`: Option[DirectiveValueParser.MaybeNumericalString] = None ) extends HasBuildRequirements { def buildRequirements: Either[BuildException, BuildRequirements] = { val versionRequirements = `==`.toSeq.map { ns => BuildRequirements.VersionEquals(ns.value, loose = true) } ++ `>=`.toSeq.map { ns => BuildRequirements.VersionHigherThan(ns.value, orEqual = true) } ++ `<=`.toSeq.map { ns => BuildRequirements.VersionLowerThan(ns.value, orEqual = true) } ++ `>`.toSeq.map { ns => BuildRequirements.VersionHigherThan(ns.value, orEqual = false) } ++ `<`.toSeq.map { ns => BuildRequirements.VersionLowerThan(ns.value, orEqual = false) } val requirements = BuildRequirements( scalaVersion = versionRequirements ) Right(requirements) } } object RequireScalaVersionBounds { val handler: DirectiveHandler[RequireScalaVersionBounds] = DirectiveHandler.derive } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/RequireScope.scala ================================================ package scala.build.preprocessing.directives import scala.build.EitherCps.{either, value} import scala.build.Ops.* import scala.build.Positioned import scala.build.directives.* import scala.build.errors.{BuildException, DirectiveErrors} import scala.build.options.{BuildRequirements, Scope} import scala.cli.commands.SpecificationLevel @DirectiveGroupName("Scope") @DirectivePrefix("target.") @DirectiveDescription("Require a scope for the current file") @DirectiveExamples("//> using target.scope test") @DirectiveUsage( "//> using target.scope _scope_", "`//> using target.scope` _scope_" ) @DirectiveLevel(SpecificationLevel.EXPERIMENTAL) final case class RequireScope( scope: Option[Positioned[String]] = None ) extends HasBuildRequirements { def buildRequirements: Either[BuildException, BuildRequirements] = either { val scopeOpt = value { scope .map { posStr => RequireScope.scopesByName.get(posStr.value).toRight { new DirectiveErrors(::("No such scope", Nil), posStr.positions) } } .sequence } BuildRequirements( scope = scopeOpt.map(BuildRequirements.ScopeRequirement(_)) ) } } object RequireScope { private lazy val scopesByName = Scope.all.map(s => s.name -> s).toMap val handler: DirectiveHandler[RequireScope] = DirectiveHandler.derive } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/Resources.scala ================================================ package scala.build.preprocessing.directives import scala.build.Positioned import scala.build.directives.* import scala.build.errors.BuildException import scala.build.options.WithBuildRequirements.* import scala.build.options.{BuildOptions, ClassPathOptions, Scope, WithBuildRequirements} import scala.cli.commands.SpecificationLevel @DirectiveGroupName("Resource directories") @DirectiveExamples("//> using resourceDir ./resources") @DirectiveExamples("//> using test.resourceDir ./resources") @DirectiveUsage( """//> using resourceDir _path_ | |//> using resourceDirs _path1_ _path2_ …""".stripMargin, """`//> using resourceDir` _path_ | |`//> using resourceDirs` _path1_ _path2_ … | |`//> using test.resourceDir` _path_ | |`//> using test.resourceDirs` _path1_ _path2_ … | |""".stripMargin ) @DirectiveDescription("Manually add a resource directory to the class path") @DirectiveLevel(SpecificationLevel.SHOULD) final case class Resources( @DirectiveName("resourceDir") resourceDirs: DirectiveValueParser.WithScopePath[List[Positioned[String]]] = DirectiveValueParser.WithScopePath.empty(Nil), @DirectiveName("test.resourceDir") @DirectiveName("test.resourceDirs") testResourceDirs: DirectiveValueParser.WithScopePath[List[Positioned[String]]] = DirectiveValueParser.WithScopePath.empty(Nil) ) extends HasBuildOptionsWithRequirements { def buildOptionsList: List[Either[BuildException, WithBuildRequirements[BuildOptions]]] = List( Resources.buildOptions(resourceDirs).map(_.withEmptyRequirements), Resources.buildOptions(testResourceDirs).map(_.withScopeRequirement(Scope.Test)) ) } object Resources { val handler: DirectiveHandler[Resources] = DirectiveHandler.derive def buildOptions(resourceDirs: DirectiveValueParser.WithScopePath[List[Positioned[String]]]) : Either[BuildException, BuildOptions] = Right { val paths = resourceDirs.value.map(_.value) val (virtualRootOpt, rootOpt) = Directive.osRootResource(resourceDirs.scopePath) // TODO Return a BuildException for malformed paths val paths0 = rootOpt .toList .flatMap { root => paths.map(os.Path(_, root)) } val virtualPaths = virtualRootOpt.map { virtualRoot => paths.map(path => virtualRoot / os.SubPath(path)) } // warnIfNotExistsPath(paths0, logger) // this should be reported elsewhere (more from BuildOptions) BuildOptions( classPathOptions = ClassPathOptions( resourcesDir = paths0, resourcesVirtualDir = virtualPaths.toList.flatten ) ) } } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/ScalaJs.scala ================================================ package scala.build.preprocessing.directives import os.Path import scala.build.Ops.EitherOptOps import scala.build.directives.* import scala.build.errors.BuildException import scala.build.internal.Constants import scala.build.options.{BuildOptions, ScalaJsMode, ScalaJsOptions} import scala.cli.commands.SpecificationLevel import scala.util.Try @DirectiveGroupName("Scala.js options") @DirectiveExamples(s"//> using jsVersion ${Constants.scalaJsVersion}") @DirectiveExamples("//> using jsMode mode") @DirectiveExamples("//> using jsNoOpt") @DirectiveExamples("//> using jsModuleKind common") @DirectiveExamples("//> using jsCheckIr") @DirectiveExamples("//> using jsEmitSourceMaps") @DirectiveExamples("//> using jsEsModuleImportMap importmap.json") @DirectiveExamples("//> using jsSmallModuleForPackage test") @DirectiveExamples("//> using jsDom") @DirectiveExamples("//> using jsHeader \"#!/usr/bin/env node\n\"") @DirectiveExamples("//> using jsAllowBigIntsForLongs") @DirectiveExamples("//> using jsAvoidClasses") @DirectiveExamples("//> using jsAvoidLetsAndConsts") @DirectiveExamples("//> using jsModuleSplitStyleStr smallestmodules") @DirectiveExamples("//> using jsEsVersionStr es2017") @DirectiveExamples("//> using jsEmitWasm") @DirectiveUsage( "//> using jsVersion|jsMode|jsModuleKind|… _value_", """ |`//> using jsVersion` _value_ | |`//> using jsMode` _value_ | |`//> using jsNoOpt` _true|false_ | |`//> using jsNoOpt` | |`//> using jsModuleKind` _value_ | |`//> using jsCheckIr` _true|false_ | |`//> using jsCheckIr` | |`//> using jsEmitSourceMaps` _true|false_ | |`//> using jsEmitSourceMaps` | |`//> using jsEsModuleImportMap` _value_ | |`//> using jsSmallModuleForPackage` _value1_ _value2_ … | |`//> using jsDom` _true|false_ | |`//> using jsDom` | |`//> using jsHeader` _value_ | |`//> using jsAllowBigIntsForLongs` _true|false_ | |`//> using jsAllowBigIntsForLongs` | |`//> using jsAvoidClasses` _true|false_ | |`//> using jsAvoidClasses` | |`//> using jsAvoidLetsAndConsts` _true|false_ | |`//> using jsAvoidLetsAndConsts` | |`//> using jsModuleSplitStyleStr` _value_ | |`//> using jsEsVersionStr` _value_ | |`//> using jsEmitWasm` _true|false_ | |`//> using jsEmitWasm` |""".stripMargin ) @DirectiveDescription("Add Scala.js options") @DirectiveLevel(SpecificationLevel.SHOULD) final case class ScalaJs( jsVersion: Option[String] = None, jsMode: Option[String] = None, jsNoOpt: Option[Boolean] = None, jsModuleKind: Option[String] = None, jsCheckIr: Option[Boolean] = None, jsEmitSourceMaps: Option[Boolean] = None, jsEsModuleImportMap: Option[String] = None, jsSmallModuleForPackage: List[String] = Nil, jsDom: Option[Boolean] = None, jsHeader: Option[String] = None, jsAllowBigIntsForLongs: Option[Boolean] = None, jsAvoidClasses: Option[Boolean] = None, jsAvoidLetsAndConsts: Option[Boolean] = None, jsModuleSplitStyleStr: Option[String] = None, jsEsVersionStr: Option[String] = None, jsEmitWasm: Option[Boolean] = None ) extends HasBuildOptions { def buildOptions: Either[BuildException, BuildOptions] = val scalaJsOptions = ScalaJsOptions( version = jsVersion, mode = ScalaJsMode(jsMode), moduleKindStr = jsModuleKind, checkIr = jsCheckIr, emitSourceMaps = jsEmitSourceMaps.getOrElse(ScalaJsOptions().emitSourceMaps), smallModuleForPackage = jsSmallModuleForPackage, dom = jsDom, header = jsHeader, allowBigIntsForLongs = jsAllowBigIntsForLongs, avoidClasses = jsAvoidClasses, avoidLetsAndConsts = jsAvoidLetsAndConsts, moduleSplitStyleStr = jsModuleSplitStyleStr, esVersionStr = jsEsVersionStr, noOpt = jsNoOpt, jsEmitWasm = jsEmitWasm.getOrElse(false) ) def absFilePath(pathStr: String): Either[ImportMapNotFound, Path] = Try(os.Path(pathStr, os.pwd)).toEither.fold( ex => Left(ImportMapNotFound( s"""Invalid path to EsImportMap. Please check your "using jsEsModuleImportMap xxxx" directive. Does this file exist $pathStr ?""", ex )), path => os.isFile(path) && os.exists(path) match { case false => Left(ImportMapNotFound( s"""Invalid path to EsImportMap. Please check your "using jsEsModuleImportMap xxxx" directive. Does this file exist $pathStr ?""", null )) case true => Right(path) } ) val jsImportMapAsPath = jsEsModuleImportMap.map(absFilePath).sequence jsImportMapAsPath.map(_ match case None => BuildOptions(scalaJsOptions = scalaJsOptions) case Some(importmap) => BuildOptions( scalaJsOptions = scalaJsOptions.copy(remapEsModuleImportMap = Some(importmap)) )) } class ImportMapNotFound(message: String, cause: Throwable) extends BuildException(message, cause = cause) object ScalaJs { val handler: DirectiveHandler[ScalaJs] = DirectiveHandler.derive } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/ScalaNative.scala ================================================ package scala.build.preprocessing.directives import scala.build.directives.* import scala.build.errors.BuildException import scala.build.internal.Constants import scala.build.options.{BuildOptions, ScalaNativeOptions} import scala.cli.commands.SpecificationLevel @DirectiveGroupName("Scala Native options") @DirectiveExamples(s"//> using nativeGc immix") @DirectiveExamples(s"//> using nativeMode debug") @DirectiveExamples(s"//> using nativeLto full") @DirectiveExamples(s"//> using nativeVersion ${Constants.scalaNativeVersion}") @DirectiveExamples(s"//> using nativeCompile -flto=thin") @DirectiveExamples(s"//> using nativeCCompile -std=c17") @DirectiveExamples(s"//> using nativeCppCompile -std=c++17 -fcxx-exceptions") @DirectiveExamples(s"//> using nativeLinking -flto=thin") @DirectiveExamples(s"//> using nativeClang ./clang") @DirectiveExamples(s"//> using nativeClangPP ./clang++") @DirectiveExamples(s"//> using nativeEmbedResources") @DirectiveExamples(s"//> using nativeEmbedResources true") @DirectiveExamples(s"//> using nativeTarget library-dynamic") @DirectiveExamples(s"//> using nativeMultithreading") @DirectiveExamples(s"//> using nativeMultithreading false") @DirectiveUsage( "//> using nativeGc _value_ | using native-version _value_", """`//> using nativeGc` **immix**_|commix|boehm|none_ | |`//> using nativeMode` **debug**_|release-fast|release-size|release-full_ | |`//> using nativeLto` **none**_|full|thin_ | |`//> using nativeVersion` _value_ | |`//> using nativeCompile` _value1_ _value2_ … | |`//> using nativeCCompile` _value1_ _value2_ … | |`//> using nativeCppCompile` _value1_ _value2_ … | |`//> using nativeLinking` _value1_ _value2_ … | |`//> using nativeClang` _value_ | |`//> using nativeClangPP` _value_ | |`//> using nativeClangPp` _value_ | |`//> using nativeEmbedResources` _true|false_ | |`//> using nativeEmbedResources` | |`//> using nativeTarget` _application|library-dynamic|library-static_ | |`//> using nativeMultithreading` _true|false_ | |`//> using nativeMultithreading` """.stripMargin.trim ) @DirectiveDescription("Add Scala Native options") @DirectiveLevel(SpecificationLevel.SHOULD) final case class ScalaNative( nativeGc: Option[String] = None, nativeMode: Option[String] = None, nativeLto: Option[String] = None, nativeVersion: Option[String] = None, nativeCompile: List[String] = Nil, nativeCCompile: List[String] = Nil, nativeCppCompile: List[String] = Nil, nativeLinking: List[String] = Nil, nativeClang: Option[String] = None, @DirectiveName("nativeClangPp") nativeClangPP: Option[String] = None, nativeEmbedResources: Option[Boolean] = None, nativeTarget: Option[String] = None, nativeMultithreading: Option[Boolean] = None ) extends HasBuildOptions { def buildOptions: Either[BuildException, BuildOptions] = { val nativeOptions = ScalaNativeOptions( gcStr = nativeGc, modeStr = nativeMode, ltoStr = nativeLto, version = nativeVersion, compileOptions = nativeCompile, cCompileOptions = nativeCCompile, cppCompileOptions = nativeCppCompile, linkingOptions = nativeLinking, clang = nativeClang, clangpp = nativeClangPP, embedResources = nativeEmbedResources, buildTargetStr = nativeTarget, multithreading = nativeMultithreading ) val buildOpt = BuildOptions(scalaNativeOptions = nativeOptions) Right(buildOpt) } } object ScalaNative { val handler: DirectiveHandler[ScalaNative] = DirectiveHandler.derive } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/ScalaVersion.scala ================================================ package scala.build.preprocessing.directives import scala.build.directives.* import scala.build.errors.BuildException import scala.build.options.{BuildOptions, MaybeScalaVersion, ScalaOptions} import scala.cli.commands.SpecificationLevel @DirectiveGroupName("Scala version") @DirectiveExamples("//> using scala 3.0.2") @DirectiveExamples("//> using scala 2.13") @DirectiveExamples("//> using scala 2") @DirectiveExamples("//> using scala 2.13.6 2.12.16") @DirectiveUsage( "//> using scala _version_+", "`//> using scala` _version_+" ) @DirectiveDescription("Set the default Scala version") @DirectiveLevel(SpecificationLevel.MUST) final case class ScalaVersion( scala: List[DirectiveValueParser.MaybeNumericalString] = Nil ) extends HasBuildOptions { def buildOptions: Either[BuildException, BuildOptions] = scala match { case Nil => Right(BuildOptions()) case first :: others => val buildOpt = BuildOptions( scalaOptions = ScalaOptions( scalaVersion = Some(MaybeScalaVersion(first.value)), extraScalaVersions = others.map(_.value).toSet ) ) Right(buildOpt) } } object ScalaVersion { val handler: DirectiveHandler[ScalaVersion] = DirectiveHandler.derive } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/ScalacOptions.scala ================================================ package scala.build.preprocessing.directives import scala.build.Positioned import scala.build.directives.* import scala.build.errors.BuildException import scala.build.options.WithBuildRequirements.* import scala.build.options.{ BuildOptions, ScalaOptions, ScalacOpt, Scope, ShadowingSeq, WithBuildRequirements } import scala.cli.commands.SpecificationLevel @DirectiveGroupName("Compiler options") @DirectiveExamples("//> using option -Xasync") @DirectiveExamples("//> using options -Xasync -Xfatal-warnings") @DirectiveExamples("//> using test.option -Xasync") @DirectiveExamples("//> using test.options -Xasync -Xfatal-warnings") @DirectiveUsage( "using option _option_ | using options _option1_ _option2_ …", """`//> using scalacOption` _option_ | |`//> using option` _option_ | |`//> using scalacOptions` _option1_ _option2_ … | |`//> using options` _option1_ _option2_ … | |`//> using test.scalacOption` _option_ | |`//> using test.option` _option_ | |`//> using test.scalacOptions` _option1_ _option2_ … | |`//> using test.options` _option1_ _option2_ … | |""".stripMargin ) @DirectiveDescription("Add Scala compiler options") @DirectiveLevel(SpecificationLevel.MUST) final case class ScalacOptions( @DirectiveName("option") @DirectiveName("scalacOption") @DirectiveName("scalacOptions") options: List[Positioned[String]] = Nil, @DirectiveName("test.option") @DirectiveName("test.options") @DirectiveName("test.scalacOption") @DirectiveName("test.scalacOptions") testOptions: List[Positioned[String]] = Nil ) extends HasBuildOptionsWithRequirements { def buildOptionsList: List[Either[BuildException, WithBuildRequirements[BuildOptions]]] = List( ScalacOptions.buildOptions(options).map(_.withEmptyRequirements), ScalacOptions.buildOptions(testOptions).map(_.withScopeRequirement(Scope.Test)) ) } object ScalacOptions { val handler: DirectiveHandler[ScalacOptions] = DirectiveHandler.derive def buildOptions(options: List[Positioned[String]]): Either[BuildException, BuildOptions] = Right { BuildOptions( scalaOptions = ScalaOptions( scalacOptions = ShadowingSeq.from(options.map(_.map(ScalacOpt(_)))) ) ) } } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/ScopedDirective.scala ================================================ package scala.build.preprocessing.directives import scala.build.Position import scala.build.errors.UnusedDirectiveError import scala.build.preprocessing.ScopePath case class ScopedDirective( directive: StrictDirective, maybePath: Either[String, os.Path], cwd: ScopePath ) { def unusedDirectiveError: UnusedDirectiveError = { val values = DirectiveUtil.concatAllValues(this) val keyPos = Position.File( maybePath, (directive.startLine, directive.startColumn), (directive.startLine, directive.startColumn + directive.key.length()) ) new UnusedDirectiveError( directive.key, values, keyPos ) } } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/Sources.scala ================================================ package scala.build.preprocessing.directives import scala.build.EitherCps.{either, value} import scala.build.Ops.* import scala.build.Positioned import scala.build.directives.* import scala.build.errors.{BuildException, CompositeBuildException, WrongSourcePathError} import scala.build.options.{BuildOptions, InternalOptions} import scala.cli.commands.SpecificationLevel @DirectiveGroupName("Custom sources") @DirectiveExamples("//> using file utils.scala") @DirectiveExamples( "//> using file https://raw.githubusercontent.com/softwaremill/sttp/refs/heads/master/examples/src/main/scala/sttp/client4/examples/json/GetAndParseJsonCatsEffectCirce.scala" ) @DirectiveUsage( "`//> using file `_path_ | `//> using files `_path1_ _path2_ …", """`//> using file` _path_ | |`//> using files` _path1_ _path2_ … |""".stripMargin ) @DirectiveDescription( "Manually add sources to the project. Does not support chaining, sources are added only once, not recursively." ) @DirectiveLevel(SpecificationLevel.SHOULD) final case class Sources( @DirectiveName("file") files: DirectiveValueParser.WithScopePath[List[Positioned[String]]] = DirectiveValueParser.WithScopePath.empty(Nil) ) extends HasBuildOptions { private def codeFile(codeFile: String, root: os.Path): Sources.CodeFile = scala.util.Try { val uri = java.net.URI.create(codeFile) uri.getScheme match { case "file" | "http" | "https" => uri } }.getOrElse { os.Path(codeFile, root) } def buildOptions: Either[BuildException, BuildOptions] = either { val paths = files .value .map { positioned => for { root <- Directive.osRoot(files.scopePath, positioned.positions.headOption) path <- { try Right(positioned.map(codeFile(_, root))) catch { case e: IllegalArgumentException => Left(new WrongSourcePathError(positioned.value, e, positioned.positions)) } } } yield path } .sequence .left.map(CompositeBuildException(_)) BuildOptions( internal = InternalOptions( extraSourceFiles = value(paths) ) ) } } object Sources { type CodeFile = os.Path | java.net.URI val handler: DirectiveHandler[Sources] = DirectiveHandler.derive } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/StrictDirective.scala ================================================ package scala.build.preprocessing.directives import com.virtuslab.using_directives.custom.model.{EmptyValue, Value} import scala.build.Position /** Represents a directive with a key and a sequence of values. * * @param key * the key of the directive * @param values * the sequence of values of the directive * @param startColumn * the column where the key of the directive starts */ case class StrictDirective( key: String, values: Seq[Value[?]], startColumn: Int = 0, startLine: Int = 0 ) { override def toString: String = { val suffix = if validValues.isEmpty then "" else s" ${validValues.mkString(" ")}" s"//> using $key$suffix" } private def validValues = values.filter { case _: EmptyValue => false case _ => true } /** Checks whether the directive with the sequence of values will fit into the given column limit, * if it does then the function returns the single directive with all the values. If the * directive does not fit then the function explodes it into a sequence of directives with * distinct values, each with a single value. */ def explodeToStringsWithColLimit(colLimit: Int = 100): Seq[String] = { val validValues = values.filter { case _: EmptyValue => false case _ => true } val usingKeyString = s"//> using $key" if (validValues.isEmpty) Seq(usingKeyString) else { val distinctValuesStrings = validValues .map { case s if s.toString.exists(_.isWhitespace) => s"\"$s\"" case s => s.toString } .distinct .sorted if (distinctValuesStrings.map(_.length).sum + usingKeyString.length < colLimit) Seq(s"$usingKeyString ${distinctValuesStrings.mkString(" ")}") else distinctValuesStrings.map(v => s"$usingKeyString $v") } } def stringValuesCount: Int = validValues.length def toStringValues: Seq[String] = validValues.map(_.toString) def position(path: Either[String, os.Path]): Position.File = values.lastOption .map { v => val position = DirectiveUtil.position(v, path) v match case _: EmptyValue => position.startPos case v if DirectiveUtil.isWrappedInDoubleQuotes(v) => position.endPos._1 -> (position.endPos._2 + 1) case _ => position.endPos }.map { (line, endColumn) => Position.File( path, (line, startColumn), (line, endColumn) ) }.getOrElse(Position.File(path, (0, 0), (0, 0))) } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/Tests.scala ================================================ package scala.build.preprocessing.directives import scala.build.Positioned import scala.build.directives.* import scala.build.errors.BuildException import scala.build.options.{BuildOptions, TestOptions} import scala.cli.commands.SpecificationLevel @DirectiveGroupName("Test framework") @DirectiveExamples("//> using testFramework utest.runner.Framework") @DirectiveExamples("//> using test.frameworks utest.runner.Framework munit.Framework") @DirectiveUsage( """using testFramework _class_name_ | |using testFrameworks _class_name_ _another_class_name_ | |using test.framework _class_name_ | |using test.frameworks _class_name_ _another_class_name_""".stripMargin, "`//> using testFramework` _class-name_" ) @DirectiveDescription("Set the test framework") @DirectiveLevel(SpecificationLevel.SHOULD) final case class Tests( @DirectiveName("testFramework") @DirectiveName("test.framework") @DirectiveName("test.frameworks") testFrameworks: Seq[Positioned[String]] = Nil ) extends HasBuildOptions { def buildOptions: Either[BuildException, BuildOptions] = { val buildOpt = BuildOptions( testOptions = TestOptions( frameworks = testFrameworks ) ) Right(buildOpt) } } object Tests { val handler: DirectiveHandler[Tests] = DirectiveHandler.derive } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/Toolkit.scala ================================================ package scala.build.preprocessing.directives import coursier.version.Version import dependency.* import scala.build.Positioned import scala.build.directives.* import scala.build.errors.BuildException import scala.build.internal.Constants import scala.build.options.* import scala.build.options.WithBuildRequirements.* import scala.cli.commands.SpecificationLevel @DirectiveGroupName("Toolkit") @DirectiveExamples(s"//> using toolkit ${Constants.toolkitDefaultVersion}") @DirectiveExamples("//> using toolkit default") @DirectiveExamples("//> using test.toolkit default") @DirectiveUsage( "//> using toolkit _version_", """`//> using toolkit` _version_ | |`//> using test.toolkit` _version_ |""".stripMargin ) @DirectiveDescription( s"Use a toolkit as dependency (not supported in Scala 2.12), 'default' version for Scala toolkit: ${Constants.toolkitDefaultVersion}, 'default' version for typelevel toolkit: ${Constants.typelevelToolkitDefaultVersion}" ) @DirectiveLevel(SpecificationLevel.SHOULD) final case class Toolkit( toolkit: Option[Positioned[String]] = None, @DirectiveName("test.toolkit") testToolkit: Option[Positioned[String]] = None ) extends HasBuildOptionsWithRequirements { def buildOptionsList: List[Either[BuildException, WithBuildRequirements[BuildOptions]]] = Toolkit.buildOptionsWithScopeRequirement(toolkit, defaultScope = None) ++ Toolkit.buildOptionsWithScopeRequirement(testToolkit, defaultScope = Some(Scope.Test)) } object Toolkit { val typelevel = "typelevel" val scala = "scala" def maxScalaNativeWarningMsg( toolkitName: String, toolkitVersion: String, maxNative: String ): String = s"$toolkitName $toolkitVersion does not support Scala Native ${Constants.scalaNativeVersion}, $maxNative should be used instead." object TypelevelToolkit { def unapply(s: Option[String]): Boolean = s.contains(typelevel) || s.contains(Constants.typelevelOrganization) } object ScalaToolkit { def unapply(s: Option[String]): Boolean = s.isEmpty || s.contains(Constants.toolkitOrganization) || s.contains(scala) } case class ToolkitDefinitions( isScalaToolkitDefault: Boolean = false, scalaToolkitExplicitVersion: Option[String] = None, isTypelevelToolkitDefault: Boolean = false, typelevelToolkitExplicitVersion: Option[String] = None ) /** @param toolkitCoords * the toolkit coordinates * @return * the `toolkit` and `toolkit-test` dependencies with the appropriate build requirements */ def resolveDependenciesWithRequirements(toolkitCoords: Positioned[String]): List[( WithBuildRequirements[Positioned[DependencyLike[NameAttributes, NameAttributes]]], ToolkitDefinitions )] = toolkitCoords match case Positioned(positions, coords) => val tokens = coords.split(':') val rawVersion = tokens.last def isDefault = rawVersion == "default" val notDefaultVersion = if rawVersion == "latest" then "latest.release" else rawVersion val flavor = tokens.dropRight(1).headOption val (org, v, trv: ToolkitDefinitions) = flavor match { case TypelevelToolkit() => val typelevelToolkitVersion = if isDefault then Constants.typelevelToolkitDefaultVersion else notDefaultVersion val explicitVersion = if isDefault then None else Some(typelevelToolkitVersion) ( Constants.typelevelOrganization, typelevelToolkitVersion, ToolkitDefinitions( isTypelevelToolkitDefault = isDefault, typelevelToolkitExplicitVersion = explicitVersion ) ) case ScalaToolkit() | None => val scalaToolkitVersion = if isDefault then Constants.toolkitDefaultVersion else notDefaultVersion val explicitVersion = if isDefault then None else Some(scalaToolkitVersion) ( Constants.toolkitOrganization, scalaToolkitVersion, ToolkitDefinitions( isScalaToolkitDefault = isDefault, scalaToolkitExplicitVersion = explicitVersion ) ) case Some(org) => (org, notDefaultVersion, ToolkitDefinitions()) } List( Positioned(positions, dep"$org::${Constants.toolkitName}::$v,toolkit") .withEmptyRequirements -> trv, Positioned(positions, dep"$org::${Constants.toolkitTestName}::$v,toolkit") .withScopeRequirement(Scope.Test) -> trv ) val handler: DirectiveHandler[Toolkit] = DirectiveHandler.derive /** Returns the `toolkit` (and potentially `toolkit-test`) dependency with the appropriate * requirements. * * If [[defaultScope]] == None, it yields a List of up to 2 instances [[WithBuildRequirements]] * of [[BuildOptions]], one with the `toolkit` dependency and no requirements, and one with the * `toolkit-test` dependency and test scope requirements. * * If [[defaultScope]] == Some([[Scope.Test]]), then it yields a List with a single instance * containing both dependencies and the test scope requirement. * * @param t * toolkit coordinates * @param defaultScope * the scope requirement for the `toolkit` dependency * @return * a list of [[Either]] [[BuildException]] [[WithBuildRequirements]] [[BuildOptions]] * containing the `toolkit` and `toolkit-test` dependencies. */ private def buildOptionsWithScopeRequirement( t: Option[Positioned[String]], defaultScope: Option[Scope] ): List[Either[BuildException, WithBuildRequirements[BuildOptions]]] = t .toList .flatMap(resolveDependenciesWithRequirements) // resolve dependencies .map { case ( WithBuildRequirements(requirements, positionedDep), ToolkitDefinitions( isScalaToolkitDefault, explicitScalaToolkitVersion, isTypelevelToolkitDefault, _ ) ) => val scalaToolkitMaxNativeVersions = if isScalaToolkitDefault then List(Constants.toolkitMaxScalaNative -> maxScalaNativeWarningMsg( toolkitName = "Scala Toolkit", toolkitVersion = Constants.toolkitDefaultVersion, maxNative = Constants.toolkitMaxScalaNative )) else explicitScalaToolkitVersion.toList .map(Version(_)) .filter(_ <= Version(Constants.toolkitVersionForNative04)) .flatMap(v => List(Constants.scalaNativeVersion04 -> maxScalaNativeWarningMsg( toolkitName = "Scala Toolkit", toolkitVersion = v.toString(), Constants.scalaNativeVersion04 )) ) val typelevelToolkitMaxNativeVersions = if isTypelevelToolkitDefault then List(Constants.typelevelToolkitMaxScalaNative -> maxScalaNativeWarningMsg( toolkitName = "TypeLevel Toolkit", toolkitVersion = Constants.typelevelToolkitDefaultVersion, maxNative = Constants.typelevelToolkitMaxScalaNative )) else Nil val maxNativeVersions = (scalaToolkitMaxNativeVersions ++ typelevelToolkitMaxNativeVersions).distinct positionedDep .withBuildRequirements { if requirements.scope.isEmpty then // if the scope is not set, set it to the default requirements.copy(scope = defaultScope.map(_.asScopeRequirement)) else requirements } .map { dep => BuildOptions( classPathOptions = ClassPathOptions( extraDependencies = ShadowingSeq.from(List(dep)) ), scalaNativeOptions = ScalaNativeOptions( maxDefaultNativeVersions = maxNativeVersions ) ) } } .groupBy(_.requirements.scope.map(_.scope)) .toList .map { (scope: Option[Scope], boWithReqsList: List[WithBuildRequirements[BuildOptions]]) => Right { boWithReqsList.foldLeft { // merge all the BuildOptions with the same scope requirement scope .map(s => BuildOptions.empty.withScopeRequirement(s)) .getOrElse(BuildOptions.empty.withEmptyRequirements) } { (acc, boWithReqs) => acc.map(_.orElse(boWithReqs.value)) } } } } ================================================ FILE: modules/directives/src/main/scala/scala/build/preprocessing/directives/Watching.scala ================================================ package scala.build.preprocessing.directives import scala.build.Positioned import scala.build.directives.* import scala.build.errors.BuildException import scala.build.options.{BuildOptions, WatchOptions} import scala.cli.commands.SpecificationLevel @DirectiveGroupName("Watch additional inputs") @DirectiveExamples("//> using watching ./data") @DirectiveUsage( """//> using watching _path_ | |//> using watching _path1_ _path2_ …""".stripMargin, """`//> using watching` _path_ | |`//> using watching` _path1_ _path2_ … | |""".stripMargin ) @DirectiveDescription("Watch additional files or directories when using watch mode") @DirectiveLevel(SpecificationLevel.EXPERIMENTAL) final case class Watching( watching: DirectiveValueParser.WithScopePath[List[Positioned[String]]] = DirectiveValueParser.WithScopePath.empty(Nil) ) extends HasBuildOptions { def buildOptions: Either[BuildException, BuildOptions] = Watching.buildOptions(watching) } object Watching { val handler: DirectiveHandler[Watching] = DirectiveHandler.derive def buildOptions( watching: DirectiveValueParser.WithScopePath[List[Positioned[String]]] ): Either[BuildException, BuildOptions] = Right { val paths = watching.value.map(_.value) val (_, rootOpt) = Directive.osRootResource(watching.scopePath) val resolvedPaths = rootOpt.toList.flatMap { root => paths.map(os.Path(_, root)) } BuildOptions( watchOptions = WatchOptions( extraWatchPaths = resolvedPaths ) ) } } ================================================ FILE: modules/docs-tests/README.md ================================================ # Sclicheck - simple tool to verify scala-cli cookbooks Sclicheck `[sklicheck]` is a simple command line tool to verify documentation. It uses regexes under the hood so in some cases we may not parse the file properly. Sclicheck extracts commands from `.md` file and then run this commands as defined within a file using a single workspace. Commands are not run in isolation by design. The whole point of the tool is to maintain a state of the current example and modify / test it using commands. Currently following commands are supported: - [Write code](#write-code) - stores snippet into a file - [Run code](#run-code) - runs provided commands as a bash script - [Check output](#check-output) - check if lates optput contains provided patterns - [Clear](#clear) - clears all file in workspace Usually, within a doc one want to first define a code, then run some commands (e.g. compilation) to test it and then check the output from last command (e.g. compilation). Sclicheck accepts following arguments: `[--dest ] [--step] [--stopAtFailure] ` where: - `` - list of `.md` files or directories to check. In case of directories Sclicheck will recusievly check all `.md` files - `--dest `- Sclicheck after checking given file (``) store all of the generated sources as well as `` (as `Readme.md`) file in `/`. This is usefull to generate examples directly from documents - `--step` - stops after each command. Useful for debugging. - `--stopAtFailure` - stops after each failed command. Useful for debugging. ## Running from the Scala CLI sources Sclicheck can be run from the Scala CLI sources root with ```text $ ./mill -i 'docs-tests[]'.run …args… ``` ## Example Let consider this simple document we want to check: ````md # Testing cat command Cat command can print a content of a file. Let's start with simple file ```md title=a.txt A text ``` Let's read it using `cat`: ```bash cat a.txt ``` `cat` fails if file does not exists: ```bash fail cat no_a_file ``` ```` For the example above Sclicheck will: - write a file `a.txt` with `A text` as content - runs `cat a.txt` and store output (`A text`) - check if a patten `A text` exisits in output from last command (`A text`) - runs `cat no_a_file` (expecting that command will fail) - check if a patten `no_a_file` exisits in output from last command (`ls: cannot access 'no_a_file': No such file or directory`) ## Actions ### Write code It extracts code to file in workspace for all code snippets marked with ` ``` title= ` for example: ```` ```scala title=A.scala def a = 123 ``` ```` Will create (or override if exists) file `A.scala` with provided context. We support writing into subdirectories as well using `title=dir/file.ext`. Sclicheck generates the sources for `.scala` and `.java` files in such a way that the lines with actual code matches the lines in provided .md files to make debugging easier. **Important!** Code block is ignored if any additional properties are passed to first line of a snippet. To add a named code snippet that should be ignore provide any additional option like `ignore` for example: ```` ```scala title=A.scala ignore def a = 123 ``` ```` ### Running bash scripts It will run code snippets starting with ` ```bash ` for example: ```bash scala-cli clean scala-cli compile . ``` The output from last command is stored for following [check output](#check-output) commands. We turn such snippet into a bash script so example below becomes: ```bash #!/usr/bin/env bash set -e scala-cli clean scala-cli compile . ``` Sclicheck expect that script return 0 but when `fail` is provided it expects failure (return non-zero exit code). Example: ```` ```bash fail ls non_exisiting_dir ``` ```` **Important** Code block is ignored if any additional properties are passed to first line of a snippet. To add a `bash` code snippet that should be ignored any additional option like `ignore` for example: ```` ```bash ignore ls non_exisiting_dir ``` ```` # Check output Sclicheck can check the output latest run command For that we use html comments starting with: - ` ``` Sclicheck, for each provided pattern check if there is at least a one line in the output that contains the pattern (for non-regex) or matches the provided regex. ## Clear In some cases we want to start with fresh context. For such cases Sclicheck provides a Clear command. It is defined as single line html comment containing single word `clear`: `` ================================================ FILE: modules/docs-tests/src/main/scala/sclicheck/sclicheck.scala ================================================ package sclicheck import fansi.Color.{Blue, Green, Red} import java.io.File import java.security.SecureRandom import scala.annotation.tailrec import scala.io.StdIn.readLine import scala.util.Random import scala.util.matching.Regex val SnippetBlock = """ *(`{2}`+)[^ ]+ title=([\w\d.\-/_]+) *""".r val CompileBlock = """ *(`{2}`+) *(\w+) +(compile|fail) *(?:title=([\w\d.\-/_]+))? *(power)? *""".r def compileBlockEnds(backticks: String) = s""" *$backticks *""".r val BashCommand = """ *```bash *(fail|run-fail)? *(clean)? *""".r val CheckBlock = """ *\<\!-- Expected(-regex)?: *""".r val CheckBlockEnd = """ *\--> *""".r val Clear = """ *", "") ++ withoutFrontMatter os.write(exampleDir / "README.md", readmeLines.mkString("\n")) os.list(out).filter(_.last.endsWith(".scala")).foreach(p => os.copy.into(p, exampleDir)) @main def check(args: String*): Unit = def processFiles(options: Options): Unit = val paths = options.files.map { str => val path = os.Path(str, os.pwd) assert(os.exists(path), s"Provided path $str does not exists in ${os.pwd}") path } val testCases = paths.flatMap(checkPath(options)) val (failed, ok) = testCases.partition(_.failure.nonEmpty) if testCases.size > 1 then if ok.nonEmpty then println(Green("Completed:")) val lines = ok.map(tc => s"\t${Green(tc.path.toString)}") println(lines.mkString("\n")) println("") if failed.nonEmpty then println(Red("Failed:")) val lines = failed.map(tc => s"\t${Red(tc.path.toString)}: ${tc.failure.get}") println(lines.mkString("\n")) println("") sys.exit(1) options.statusFile.foreach { file => os.write.over(file, s"Test completed:\n${testCases.map(_.path).mkString("\n")}") } case class PathParameter(name: String): def unapply(args: Seq[String]): Option[(os.Path, Seq[String])] = args.match case `name` :: param :: tail => if param.startsWith("--") then println(s"Please provide file name not an option: $param") sys.exit(1) Some((os.Path(param, os.pwd), tail)) case `name` :: Nil => println(Red(s"Expected an argument after `--$name` parameter")) sys.exit(1) case _ => None val Dest = PathParameter("--dest") val StatusFile = PathParameter("--status-file") @tailrec def parseArgs(args: Seq[String], options: Options): Options = args match case Nil => options case "--step" :: rest => parseArgs(rest, options.copy(step = true)) case "--stopAtFailure" :: rest => parseArgs(rest, options.copy(stopAtFailure = true)) case Dest(dest, rest) => parseArgs(rest, options.copy(dest = Some(dest))) case StatusFile(file, rest) => parseArgs(rest, options.copy(statusFile = Some(file))) case path :: rest => parseArgs(rest, options.copy(files = options.files :+ path)) val options = Options( scalaCliCommand = Seq(Option(System.getenv("SCLICHECK_SCALA_CLI")).getOrElse("scala-cli")) ) processFiles(parseArgs(args, options)) ================================================ FILE: modules/docs-tests/src/test/resources/test.md ================================================ # Testing cat command This is test document for Sclicheck Cat command can print a content of a file. Let's start with simple file ```md title=a.txt A text ``` Let's read it using `cat`: ```bash cat a.txt ``` `cat` fails if file does not exists: ```bash fail cat no_a_file ``` ================================================ FILE: modules/docs-tests/src/test/scala/sclicheck/DocTests.scala ================================================ package sclicheck import java.util.concurrent.TimeUnit import scala.concurrent.duration.FiniteDuration import scala.util.matching.Regex class DocTests extends munit.FunSuite { override def munitTimeout = new FiniteDuration(600, TimeUnit.SECONDS) case class DocTestEntry(name: String, path: os.Path, depth: Int = Int.MaxValue) val docsRootPath: os.Path = os.Path(sys.env("MILL_WORKSPACE_ROOT")) / "website" / "docs" val entries: Seq[DocTestEntry] = Seq( DocTestEntry("root", docsRootPath, depth = 1), DocTestEntry("cookbook", docsRootPath / "cookbooks"), DocTestEntry("command", docsRootPath / "commands"), DocTestEntry("guide", docsRootPath / "guides"), DocTestEntry("reference", docsRootPath / "reference") ) val options: Options = Options(scalaCliCommand = Seq(TestUtil.scalaCliPath.toString)) private val ReleaseNotesMd = os.rel / "release_notes.md" /** `## [v1.12.0](https://…)` style headings that delimit per-version release note sections. */ private val ReleaseVersionHeading: Regex = """^##\s+\[(v[\w.\-]+)\]""".r private def isReleaseVersionHeadingLine(line: String): Boolean = ReleaseVersionHeading.findFirstMatchIn(line).nonEmpty private def lineContainsAnyChecks(l: String): Boolean = l.startsWith("```md") || l.startsWith("```bash") || l.startsWith("```scala compile") || l.startsWith("```scala fail") || l.startsWith("````markdown compile") || l.startsWith("````markdown fail") || l.startsWith("```java compile") || l.startsWith("````java fail") private def fileContainsAnyChecks(f: os.Path): Boolean = os.read.lines(f).exists(lineContainsAnyChecks) /** One sclicheck run per `## [v…]` section so each gets its own timeout and workspace (Option C). */ private def releaseNotesSections(file: os.Path): Seq[(String, IndexedSeq[String])] = val lines = os.read.lines(file).toIndexedSeq val starts = lines.zipWithIndex.collect { case (line, i) if isReleaseVersionHeadingLine(line) => i } if starts.isEmpty && fileContainsAnyChecks(file) then Seq(("release_notes", lines)) else if starts.isEmpty then Nil else starts.zipWithIndex.map { case (startIdx, chunkIdx) => val endIdx = if chunkIdx + 1 < starts.size then starts(chunkIdx + 1) else lines.size val slice = if chunkIdx == 0 then lines.slice(0, endIdx) else lines.slice(startIdx, endIdx) val ver = ReleaseVersionHeading.findFirstMatchIn(lines(startIdx)).get.group(1) (ver, slice) }.filter { case (_, slice) => slice.exists(lineContainsAnyChecks) } for { DocTestEntry(tpe, dir, depth) <- entries inputs = os.walk(dir, maxDepth = depth) .filter(_.last.endsWith(".md")) .filter(os.isFile(_)) .filter(fileContainsAnyChecks) .map(_.relativeTo(dir)) .sortBy(_.toString) md <- inputs if !(tpe == "root" && md == ReleaseNotesMd) } test(s"$tpe ${md.toString.stripSuffix(".md")}") { TestUtil.retryOnCi()(checkFile(dir / md, options)) } private val releaseNotesFile = docsRootPath / "release_notes.md" if os.isFile(releaseNotesFile) && fileContainsAnyChecks(releaseNotesFile) then for (ver, slice) <- releaseNotesSections(releaseNotesFile) do val safeStem = ver.replaceAll("[^a-zA-Z0-9._\\-]", "_") test(s"root release_notes $ver") { TestUtil.retryOnCi() { TestUtil.withTmpDir("sclicheck-release-notes") { tmp => val chunkFile = tmp / s"release_notes-$safeStem.md" os.write.over(chunkFile, slice.mkString("", "\n", "\n")) checkFile(chunkFile, options) } } } } ================================================ FILE: modules/docs-tests/src/test/scala/sclicheck/GifTests.scala ================================================ package sclicheck import java.util.concurrent.TimeUnit import scala.concurrent.duration.FiniteDuration class GifTests extends munit.FunSuite { override def munitTimeout = new FiniteDuration(360, TimeUnit.SECONDS) val scenariosDir = Option(System.getenv("SCALA_CLI_GIF_SCENARIOS")).map(os.Path(_, os.pwd)).getOrElse { sys.error("SCALA_CLI_GIF_SCENARIOS not set") } val websiteImgDir = Option(System.getenv("SCALA_CLI_WEBSITE_IMG")).map(os.Path(_, os.pwd)).getOrElse { sys.error("SCALA_CLI_WEBSITE_IMG not set") } lazy val gifRenderedDockerDir = Option(System.getenv("SCALA_CLI_GIF_RENDERER_DOCKER_DIR")).map(os.Path(_, os.pwd)).getOrElse { sys.error("SCALA_CLI_GIF_RENDERER_DOCKER_DIR not set") } lazy val svgRenderedDockerDir = Option(System.getenv("SCALA_CLI_SVG_RENDERER_DOCKER_DIR")).map(os.Path(_, os.pwd)).getOrElse { sys.error("SCALA_CLI_SVG_RENDERER_DOCKER_DIR not set") } val scenarioScripts = os.list(scenariosDir) .filter(!_.last.startsWith(".")) .filter(_.last.endsWith(".sh")) .filter(os.isFile(_)) lazy val hasTty = os.proc("tty").call(stdin = os.Inherit, stdout = os.Inherit, check = false).exitCode == 0 lazy val ttyOpts = if (hasTty) Seq("-it") else Nil def buildImages = true def forceBuildImages = true def columns = 70 def rows = 20 def record = true def gifs = true def svgs = true def maybeBuildImages(): Unit = if (buildImages || forceBuildImages) { def hasImage(imageName: String): Boolean = { val res = os.proc("docker", "images", "-q", imageName).call() res.out.trim().nonEmpty } if (forceBuildImages || !hasImage("gif-renderer")) val scalaCliJvmPath = gifRenderedDockerDir / "scala-cli-jvm" os.copy.over(TestUtil.scalaCliPath, scalaCliJvmPath) os.proc("docker", "build", gifRenderedDockerDir, "--tag", "gif-renderer") .call(stdin = os.Inherit, stdout = os.Inherit) os.remove(scalaCliJvmPath) if (forceBuildImages || !hasImage("svg_rendrer")) os.proc("docker", "build", svgRenderedDockerDir, "--tag", "svg_rendrer") .call(stdin = os.Inherit, stdout = os.Inherit) } for (script <- scenarioScripts) { val name = script.last.stripSuffix(".sh") test(name) { maybeBuildImages() TestUtil.withTmpDir(s"scala-cli-gif-test-$name") { out => try { if (record) os.proc( "docker", "run", "--rm", ttyOpts, "-v", s"$out/.scala:/data/out", "gif-renderer", "./run_scenario.sh", name ) .call(stdin = os.Inherit, stdout = os.Inherit) if (hasTty) { val svgRenderMappings = Seq("-v", s"$websiteImgDir:/data", "-v", s"$out/.scala:/out") if (svgs) { val svgRenderOps = Seq( "--in", s"/out/$name.cast", "--width", columns.toString, "--height", rows.toString, "--term", "iterm2", "--padding", "20" ) os.proc( "docker", "run", "--rm", svgRenderMappings, "svg_rendrer", "a", svgRenderOps, "--out", s"/data/$name.svg", "--profile", "/profiles/light" ) .call(stdin = os.Inherit, stdout = os.Inherit) os.proc( "docker", "run", "--rm", svgRenderMappings, "svg_rendrer", "a", svgRenderOps, "--out", s"/data/dark/$name.svg", "--profile", "/profiles/dark" ) .call(stdin = os.Inherit, stdout = os.Inherit) } if (gifs) { os.proc( "docker", "run", "--rm", svgRenderMappings, "asciinema/asciicast2gif", "-w", columns, "-h", rows, "-t", "monokai", s"/out/$name.cast", s"/data/gifs/$name.gif" ) .call(stdin = os.Inherit, stdout = os.Inherit) os.proc( "docker", "run", "--rm", svgRenderMappings, "asciinema/asciicast2gif", "-w", columns, "-h", rows, "-t", "solarized-dark", s"/out/$name.cast", s"/data/dark/gifs/$name.gif" ) .call(stdin = os.Inherit, stdout = os.Inherit) } } } finally // Clean-up out dir with the same rights as the images above (should be root - from docker) os.proc( "docker", "run", "--rm", ttyOpts, "-v", s"$out:/out", s"alpine:${Constants.alpineVersion}", "sh", "-c", "rm -rf /out/* || true; rm -rf /out/.* || true" ) .call(stdin = os.Inherit, stdout = os.Inherit) } } } } ================================================ FILE: modules/docs-tests/src/test/scala/sclicheck/MarkdownLinkTests.scala ================================================ package sclicheck class MarkdownLinkTests extends munit.FunSuite { private val docsRootPath: os.Path = os.Path(sys.env("MILL_WORKSPACE_ROOT")) / "website" / "docs" private val markdownLinkPattern = """\[([^\]]*)\]\(([^)]+)\)""".r private def stripFragment(url: String): String = url.split('#').head private def hasFileExtension(url: String): Boolean = { val lastSegment = stripFragment(url).split('/').last lastSegment.contains('.') } private def isRelativeDocLink(url: String): Boolean = url.startsWith(".") && !url.contains("://") private def hasDoubleSlash(url: String): Boolean = url.replace("://", "").contains("//") private def mdFileExistsFor(sourceFile: os.Path, relativeUrl: String): Boolean = { val dir = sourceFile / os.up val targetPath = stripFragment(relativeUrl) try os.isFile(dir / os.RelPath(targetPath + ".md")) catch { case _: Exception => false } } case class LinkIssue(file: os.Path, line: Int, linkText: String, url: String, problem: String) private def findIssuesInFile(file: os.Path): Seq[LinkIssue] = { val lines = os.read.lines(file) val issues = Seq.newBuilder[LinkIssue] for { (lineContent, lineIdx) <- lines.zipWithIndex m <- markdownLinkPattern.findAllMatchIn(lineContent) } { val linkText = m.group(1) val url = m.group(2) if isRelativeDocLink(url) then { if hasDoubleSlash(url) then issues += LinkIssue( file, lineIdx + 1, linkText, url, "relative link contains double slash" ) if !hasFileExtension(url) && mdFileExistsFor(file, url) then issues += LinkIssue( file, lineIdx + 1, linkText, url, "relative link to doc page without .md extension" ) } } issues.result() } test("all relative doc links should use .md extension") { val allMdFiles = os.walk(docsRootPath) .filter(_.last.endsWith(".md")) .filter(os.isFile(_)) .sorted val allIssues = allMdFiles.flatMap(findIssuesInFile) if allIssues.nonEmpty then { val report = allIssues .map { issue => val relPath = issue.file.relativeTo(docsRootPath) s" $relPath:${issue.line} — [${issue.linkText}](${issue.url})\n ${issue.problem}" } .mkString("\n") fail( s"Found ${allIssues.size} relative doc link(s) with issues:\n$report\n\n" + "Relative links to other doc pages must include the .md extension, " + "otherwise they resolve incorrectly in production due to trailing slashes." ) } } } ================================================ FILE: modules/docs-tests/src/test/scala/sclicheck/SclicheckTests.scala ================================================ package sclicheck class SclicheckTests extends munit.FunSuite: test("Run regex") { assert(clue(CompileBlock.unapplySeq("``scala compile")).isEmpty) assert( clue(CompileBlock.unapplySeq("```scala compile")) .contains(Seq("```", "scala", "compile", null, null)) ) assert( clue(CompileBlock.unapplySeq("```scala compile power")) .contains(Seq("```", "scala", "compile", null, "power")) ) assert( clue(CompileBlock.unapplySeq("```scala fail")) .contains(Seq("```", "scala", "fail", null, null)) ) assert( clue(CompileBlock.unapplySeq("````markdown compile")) .contains(Seq("````", "markdown", "compile", null, null)) ) assert( clue(CompileBlock.unapplySeq("````markdown fail title=a.md")) .contains(Seq("````", "markdown", "fail", "a.md", null)) ) assert(clue(CompileBlock.unapplySeq("``scala fail title=a.sc")).isEmpty) assert( clue(CompileBlock.unapplySeq("```scala fail title=a.sc")) .contains(Seq("```", "scala", "fail", "a.sc", null)) ) } ================================================ FILE: modules/docs-tests/src/test/scala/sclicheck/TestUtil.scala ================================================ package sclicheck import scala.annotation.tailrec import scala.concurrent.duration.{DurationInt, FiniteDuration} object TestUtil { val isCI: Boolean = System.getenv("CI") != null def withTmpDir[T](prefix: String)(f: os.Path => T): T = { val tmpDir = os.temp.dir(prefix = prefix) try f(tmpDir) finally tryRemoveAll(tmpDir) } def tryRemoveAll(f: os.Path): Unit = try os.remove.all(f) catch { case ex: java.nio.file.FileSystemException => System.err.println(s"Could not remove $f ($ex), ignoring it.") } lazy val scalaCliPath = Option(System.getenv("SCLICHECK_SCALA_CLI")).map(os.Path(_, os.pwd)).getOrElse { sys.error("SCLICHECK_SCALA_CLI not set") } def retry[T]( maxAttempts: Int = 3, waitDuration: FiniteDuration = 5.seconds )( run: => T ): T = { @tailrec def helper(count: Int): T = try run catch { case t: Throwable => if (count >= maxAttempts) { System.err.println(s"$maxAttempts attempts failed, caught $t. Giving up.") throw new Exception(t) } else { val remainingAttempts = maxAttempts - count System.err.println( s"Caught $t, $remainingAttempts attempts remaining, trying again in $waitDuration…" ) Thread.sleep(waitDuration.toMillis) System.err.println(s"Trying attempt $count out of $maxAttempts...") helper(count + 1) } } helper(1) } def retryOnCi[T](maxAttempts: Int = 3, waitDuration: FiniteDuration = 5.seconds)( run: => T ): T = retry(if (isCI) maxAttempts else 1, waitDuration)(run) } ================================================ FILE: modules/dummy/amm/src/main/scala/AmmDummy.scala ================================================ /** Create an empty class to enforce resolving ivy deps by mill for `amm` module */ class AmmDummy ================================================ FILE: modules/dummy/scalafmt/src/main/scala/ScalafmtDummy.scala ================================================ /** Create an empty class to enforce resolving ivy deps by mill for `scalafmt` module */ class ScalafmtDummy ================================================ FILE: modules/generate-reference-doc/src/main/scala/scala/cli/doc/GenerateReferenceDoc.scala ================================================ package scala.cli.doc import caseapp.* import caseapp.core.Arg import caseapp.core.Scala3Helpers.* import caseapp.core.util.Formatter import munit.diff.Diff import java.nio.charset.StandardCharsets import java.util import scala.build.info.{ArtifactId, BuildInfo, ExportDependencyFormat, ScopedBuildInfo} import scala.build.internals.EnvVar import scala.build.options.{BuildOptions, BuildRequirements, WithBuildRequirements} import scala.build.preprocessing.directives.DirectiveHandler import scala.build.preprocessing.directives.DirectivesPreprocessingUtils.* import scala.cli.commands.{ScalaCommand, SpecificationLevel} import scala.cli.doc.ReferenceDocUtils.* import scala.cli.util.ArgHelpers.* import scala.cli.{ScalaCli, ScalaCliCommands} object GenerateReferenceDoc extends CaseApp[InternalDocOptions] { implicit class PBUtils(sb: StringBuilder) { def section(t: String*): StringBuilder = sb.append(t.mkString("", "\n", "\n\n")) } private def cleanUpOrigin(origin: String): String = { val origin0 = origin.takeWhile(_ != '[').stripSuffix("Options") val actualOrigin = if (origin0 == "WithFullHelp" || origin0 == "WithHelp") "Help" else origin0 if (actualOrigin == "Shared") actualOrigin else actualOrigin.stripPrefix("Shared") } private def formatOrigin(origin: String, keepCapitalization: Boolean = true): String = { val l = origin.head :: origin.toList.tail.flatMap { c => if (c.isUpper) " " :: c.toLower :: Nil else c :: Nil } val value = l.mkString .replace("Scala native", "Scala Native") .replace("Scala js", "Scala.js") .split("\\s+") .map(w => if (w == "ide") "IDE" else w) .mkString(" ") val valueNeedsLowerCasing = keepCapitalization || (value.startsWith("Scala") && !value.startsWith("Scalac")) || !value.head.isUpper if (valueNeedsLowerCasing) value else value.head.toLower +: value.tail } private def prettyPath(path: os.Path): String = if (path.startsWith(os.pwd)) path.relativeTo(os.pwd).toString else path.toString private def maybeWrite(dest: os.Path, content: String): Unit = { val content0 = content.getBytes(StandardCharsets.UTF_8) val needsUpdate = !os.exists(dest) || { val currentContent = os.read.bytes(dest) !util.Arrays.equals(content0, currentContent) } if (needsUpdate) { os.write.over(dest, content0, createFolders = true) System.err.println(s"Wrote ${prettyPath(dest)}") } else System.err.println(s"${prettyPath(dest)} doesn't need updating") } private def maybeDiff(dest: os.Path, content: String): Option[Diff] = { val currentContentOpt = if (os.exists(dest)) Some(new String(os.read.bytes(dest), StandardCharsets.UTF_8)) else None currentContentOpt.filter(_ != content).map { currentContent => new Diff(currentContent, content) } } private def actualHelp(command: Command[?]): Help[?] = command match { case ext: scala.cli.commands.pgp.ExternalCommand => ext.actualHelp case _ => command.finalHelp } private def scalacOptionForwarding = """## Scalac options forwarding | | All options that start with: | | |- `-g` |- `-language` |- `-opt` |- `-P` |- `-target` |- `-V` |- `-W` |- `-X` |- `-Y` | |are assumed to be Scala compiler options and will be propagated to Scala Compiler. This applies to all commands that uses compiler directly or indirectly. | | | ## Scalac options that are directly supported in scala CLI (so can be provided as is, without any prefixes etc.): | | - `-encoding` | - `-release` | - `-color` | - `-nowarn` | - `-feature` | - `-deprecation` | - `-indent` | - `-no-indent` | - `-unchecked` | - `-rewrite` | - `-old-syntax` | - `-new-syntax` | |""".stripMargin private def cliOptionsContent( commands: Seq[Command[?]], allArgs: Seq[Arg], nameFormatter: Formatter[Name], onlyRestricted: Boolean = false ): String = { val argsToShow = if (!onlyRestricted) allArgs else allArgs.filterNot(_.isExperimentalOrRestricted) val argsByOrigin = argsToShow.groupBy(arg => cleanUpOrigin(arg.origin.getOrElse(""))) val commandOrigins = for { command <- commands origin <- actualHelp(command).args.map(_.origin.getOrElse("")).map(cleanUpOrigin) } yield origin -> command val commandOriginsMap = commandOrigins.groupBy(_._1) .map { case (k, v) => (k, v.map(_._2).distinct.sortBy(_.name)) } // Collect all origins that are referenced by commands, even if they have no args val allReferencedOrigins = commandOriginsMap.keySet ++ argsByOrigin.keySet val mainOptionsContent = new StringBuilder val hiddenOptionsContent = new StringBuilder mainOptionsContent.append( s"""--- |title: Command-line options |sidebar_position: 1 |--- | |${ if (onlyRestricted) "**This document describes as scala-cli behaves if run as `scala` command. See more information in [SIP-46](https://github.com/scala/improvement-proposals/pull/46)**" else "" } | |This is a summary of options that are available for each subcommand of the `${ScalaCli .baseRunnerName}` command. | |""".stripMargin ) mainOptionsContent.section(scalacOptionForwarding) for (origin <- allReferencedOrigins.toVector.sortBy(identity)) { val originArgs = argsByOrigin.getOrElse(origin, Nil) val distinctArgs = originArgs.map(_.withOrigin(None)).distinct val originCommands = commandOriginsMap.getOrElse(origin, Nil) val onlyForHiddenCommands = originCommands.nonEmpty && originCommands.forall(_.hidden) // Empty option groups should not be treated as internal if they're referenced by commands val allArgsHidden = distinctArgs.nonEmpty && distinctArgs.forall(_.noHelp) val isInternal = onlyForHiddenCommands || allArgsHidden val b = if (isInternal) hiddenOptionsContent else mainOptionsContent if (originCommands.nonEmpty) { val formattedOrigin = formatOrigin(origin) val formattedCommands = originCommands.map { c => // https://scala-cli.virtuslab.org/docs/reference/commands#install-completions val names = c.names.map(_.mkString(" ")) val text = names.map("`" + _ + "`").mkString(" , ") s"[$text](./commands.md#${names.head.replace(" ", "-")})" } val availableIn = "Available in commands:\n\n" + formattedCommands.mkString(", ") val header = if (isInternal) "###" else "##" b.append( s"""$header $formattedOrigin options | |$availableIn | | | |""".stripMargin ) if (distinctArgs.nonEmpty) for (arg <- distinctArgs) { import caseapp.core.util.NameOps._ arg.name.option(nameFormatter) val names = (arg.name +: arg.extraNames).map(_.option(nameFormatter)) b.append(s"### `${names.head}`\n\n") if (names.tail.nonEmpty) b.append( names .tail .sortBy(_.dropWhile(_ == '-')) .map { case name if arg.deprecatedOptionAliases.contains(name) => s"[deprecated] `$name`" case name => s"`$name`" } .mkString("Aliases: ", ", ", "\n\n") ) if (onlyRestricted) b.section(s"`${arg.level.md}` per Scala Runner specification") else if (isInternal || arg.noHelp) b.append("[Internal]\n") for (desc <- arg.helpMessage.map(_.referenceDocMessage)) b.append( s"""$desc | |""".stripMargin ) } else // Empty option group - add a note b.append( "*This section was automatically generated and may be empty if no options were available.*\n\n" ) } } mainOptionsContent.append("## Internal options \n") mainOptionsContent.append(hiddenOptionsContent.toString) mainOptionsContent.toString } private def optionsReference( commands: Seq[Command[?]], nameFormatter: Formatter[Name] ): String = { val b = new StringBuilder b.section( """--- |title: Scala Runner specification |sidebar_position: 1 |--- """.stripMargin ) b.section( "**This document describes proposed specification for Scala runner based on Scala CLI documentation as requested per [SIP-46](https://github.com/scala/improvement-proposals/pull/46)**" ) b.section( "Commands and options are marked with MUST and SHOULD (in the RFC style) for ones applicable for Scala Runner.", "Options and commands marked as **Implementation** are needed for smooth running of Scala CLI.", "We recommend for those options and commands to be supported by the `scala` command (when based on Scala CLI) but not to be a part of the Scala Runner specification." ) b.section( "The proposed Scala runner specification should also contain supported `Using directives` defined in the dedicated [document](./directives.md)]" ) b.section(scalacOptionForwarding) def optionsForCommand(command: Command[?]): Unit = { val supportedArgs = actualHelp(command).args val argsByLevel = supportedArgs.groupBy(_.level) import caseapp.core.util.NameOps._ (SpecificationLevel.inSpecification :+ SpecificationLevel.IMPLEMENTATION).foreach { level => val args = argsByLevel.getOrElse(level, Nil) if (args.nonEmpty) { if (level == SpecificationLevel.IMPLEMENTATION) b.section( "
", s"\n### Implementantation specific options\n", "" ) else b.section(s"### ${level.md} options") args.foreach { arg => val names = (arg.name +: arg.extraNames).map(_.option(nameFormatter)) b.section(s"**${names.head}**") b.section(arg.helpMessage.fold("")(_.referenceDocMessage)) if (names.tail.nonEmpty) b.section(names.tail.mkString("Aliases: `", "` ,`", "`")) } if (level == SpecificationLevel.IMPLEMENTATION) b.section("
") } } } (SpecificationLevel.inSpecification :+ SpecificationLevel.IMPLEMENTATION).foreach { level => val levelCommands = commands.collect { case s: ScalaCommand[_] if s.scalaSpecificationLevel == level => s } if (levelCommands.nonEmpty) b.section(s"# ${level.md} commands") levelCommands.foreach { command => b.section( s"## `${command.name}` command", s"**${level.md} for Scala Runner specification.**" ) if (command.names.tail.nonEmpty) b.section(command.names.map(_.mkString(" ")).tail.mkString("Aliases: `", "`, `", "`")) for (desc <- command.messages.helpMessage.map(_.referenceDocDetailedMessage)) b.section(desc) optionsForCommand(command) b.section("---") } } b.toString } private def commandsContent(commands: Seq[Command[?]], onlyRestricted: Boolean): String = { val b = new StringBuilder b.append( s"""--- |title: Commands |sidebar_position: 3 |--- | |${ if (onlyRestricted) "**This document describes as scala-cli behaves if run as `scala` command. See more information in [SIP-46](https://github.com/scala/improvement-proposals/pull/46)**" else "" } | | |""".stripMargin ) val (hiddenCommands, mainCommands) = commands.partition(_.hidden) def addCommand(c: Command[?], additionalIndentation: Int = 0): Unit = { val origins = c.parser0.args.flatMap(_.origin.toSeq).map(cleanUpOrigin).distinct.sorted val headerPrefix = "#" * additionalIndentation val names = c.names.map(_.mkString(" ")) b.append(s"$headerPrefix## ${names.head}\n\n") if (names.tail.nonEmpty) b.append(names.tail.sorted.mkString("Aliases: `", "`, `", "`\n\n")) for (desc <- c.messages.helpMessage.map(_.referenceDocDetailedMessage)) b.section(desc) if (origins.nonEmpty) { val links = origins.map { origin => val cleanedUp = formatOrigin(origin, keepCapitalization = false) val linkPart = cleanedUp .split("\\s+") .map(_.toLowerCase(util.Locale.ROOT).filter(_ != '.')) .mkString("-") s"[$cleanedUp](./cli-options.md#$linkPart-options)" } b.append( s"""Accepts option groups: ${links.mkString(", ")} |""".stripMargin ) b.append("\n") } } if (onlyRestricted) { val scalaCommands = commands.collect { case s: ScalaCommand[_] => s } b.section("# `scala` commands") // TODO add links to RFC b.section( "This document is a specification of the `scala` runner.", "For now it uses documentation specific to Scala CLI but at some point it may be refactored to provide more abstract documentation.", "Documentation is split into sections in the spirit of RFC keywords (`MUST`, `SHOULD`, `NICE TO HAVE`) including the `IMPLEMENTATION` category,", "that is reserved for commands that need to be present for Scala CLI to work properly but should not be a part of the official API." ) SpecificationLevel.inSpecification.foreach { level => val commands = scalaCommands.filter(_.scalaSpecificationLevel == level) if (commands.nonEmpty) { b.section(s"## ${level.md.capitalize} commands:") commands.foreach(addCommand(_, additionalIndentation = 1)) } } b.section("## Implementation-specific commands") b.section( "Commands which are used within Scala CLI and should be a part of the `scala` command but aren't a part of the specification." ) scalaCommands .filter(_.scalaSpecificationLevel == SpecificationLevel.IMPLEMENTATION) .foreach(c => addCommand(c, additionalIndentation = 1)) } else { mainCommands.foreach(addCommand(_)) b.section("## Hidden commands") hiddenCommands.foreach(c => addCommand(c, additionalIndentation = 1)) } b.toString } private def usingContent( allUsingHandlers: Seq[ DirectiveHandler[BuildOptions | List[WithBuildRequirements[BuildOptions]]] ], requireHandlers: Seq[DirectiveHandler[BuildRequirements]], onlyRestricted: Boolean ): String = { val b = new StringBuilder b.section( """--- |title: Directives |sidebar_position: 2 |---""".stripMargin ) b.section( if (onlyRestricted) "**This document describes as scala-cli behaves if run as `scala` command. See more information in [SIP-46](https://github.com/scala/improvement-proposals/pull/46)**" else "## using directives" ) def addHandlers(handlers: Seq[DirectiveHandler[?]]): Unit = for (handler <- handlers.sortBy(_.name)) { b.append( s"""### ${handler.name} | |${handler.descriptionMd} | |${handler.usageMd} | |""".stripMargin ) val examples = handler.examples if (examples.nonEmpty) { b.append( """#### Examples |""".stripMargin ) for (ex <- examples) b.append( s"""`$ex` | |""".stripMargin ) } } if (onlyRestricted) { // TODO add links to RFC b.section( "This document is a specification of the `scala` runner.", "For now it uses documentation specific to Scala CLI but at some point it may be refactored to provide more abstract documentation.", "Documentation is split into sections in the spirit of RFC keywords (`MUST`, `SHOULD`)." ) SpecificationLevel.inSpecification.foreach { level => val handlers = allUsingHandlers.filter(_.scalaSpecificationLevel == level) if (handlers.nonEmpty) { b.section(s"## ${level.md.capitalize} directives:") addHandlers(handlers) } } val implHandlers = allUsingHandlers.filter(_.scalaSpecificationLevel == SpecificationLevel.IMPLEMENTATION) if (implHandlers.nonEmpty) { b.section("## Implementation-specific directices") b.section( "Directives which are used within Scala CLI and should be a part of the `scala` command but aren't a part of the specification." ) addHandlers(implHandlers) } } else { addHandlers(allUsingHandlers) b.append( """ |## target directives | |""".stripMargin ) addHandlers(requireHandlers) } b.toString } private def buildInfoContent: String = { val b = new StringBuilder b.section( """--- |title: BuildInfo |sidebar_position: 6 |---""".stripMargin ) b.section( """:::caution |BuildInfo is a restricted feature and requires setting the `--power` option to be used. |You can pass it explicitly or set it globally by running: | | scala-cli config power true |:::""".stripMargin ) b.section( """During the building process Scala CLI collects information about the project's configuration, |both from the console options and `using directives` found in the project's sources. |You can access this information from your code using the `BuildInfo` object, that's automatically generated for your |build on compile when that information changes.""".stripMargin ) b.section( """To enable BuildInfo generation pass the `--build-info` option to Scala CLI or use a |`//> using buildInfo` directive.""".stripMargin ) b.section( """## Usage | |The generated BuildInfo object is available on the project's classpath. To access it you need to import it first. |It is available in the package `scala.cli.build` so use |```scala |import scala.cli.build.BuildInfo |``` |to import it. | |Below you can find an example instance of the BuildInfo object, with all fields explained. |Some of the values have been shortened for readability.""".stripMargin ) val osLibDep = ExportDependencyFormat("com.lihaoyi", ArtifactId("os-lib", "os-lib_3"), "0.9.1") val toolkitDep = ExportDependencyFormat("org.scala-lang", ArtifactId("toolkit", "toolkit_3"), "latest.release") val mainScopedBuildInfo = ScopedBuildInfo(BuildOptions(), Seq(".../Main.scala")).copy( scalacOptions = Seq("-Werror"), scalaCompilerPlugins = Nil, dependencies = Seq(osLibDep), resolvers = Seq("https://repo1.maven.org/maven2", "ivy:file:..."), resourceDirs = Seq(".../resources"), customJarsDecls = Seq(".../AwesomeJar1.jar", ".../AwesomeJar2.jar") ) val testScopedBuildInfo = ScopedBuildInfo(BuildOptions(), Seq(".../MyTests.scala")).copy( scalacOptions = Seq("-Vdebug"), scalaCompilerPlugins = Nil, dependencies = Seq(toolkitDep), resolvers = Seq("https://repo1.maven.org/maven2", "ivy:file:..."), resourceDirs = Seq(".../test/resources"), customJarsDecls = Nil ) val generatedBuildInfo = BuildInfo(BuildOptions(), os.pwd) match { case Right(bv) => bv case Left(exception) => System.err.println(s"Failed to generate BuildInfo: ${exception.message}") sys.exit(1) } val buildInfo = generatedBuildInfo.copy( scalaVersion = Some("3.3.0"), platform = Some("JVM"), jvmVersion = Some("11"), scalaJsVersion = None, jsEsVersion = None, scalaNativeVersion = None, mainClass = Some("Main"), projectVersion = None, scalaCliVersion = Some("1.7.0") ) .withScope("main", mainScopedBuildInfo) .withScope("test", testScopedBuildInfo) b.section( s"""```scala |${buildInfo.generateContents().strip()} |```""".stripMargin ) b.section( """## Project version | |A part of the BuildInfo object is the project version. By default, an attempt is made to deduce it using git tags |of the workspace repository. If this fails (e.g. no git repository is present), the version is set to `0.1.0-SNAPSHOT`. |You can override this behaviour by passing the `--project-version` option to Scala CLI or by using a |`//> using projectVersion` directive. | |Please note that only tags that follow the semantic versioning are taken into consideration. | |Values available for project version configuration are: |- `git:tag` or `git`: use the latest stable git tag, if it is older than HEAD then try to increment it | and add a suffix `-SNAPSHOT`, if no tag is available then use `0.1.0-SNAPSHOT` |- `git:dynver`: use the latest (stable or unstable) git tag, if it is older than HEAD then use the output of | `-{distance from last tag}-g{shortened version of HEAD commit hash}-SNAPSHOT`, if no tag is available then use `0.1.0-SNAPSHOT` | |The difference between stable and unstable tags are, that the latter can contain letters, e.g. `v0.1.0-RC1`. |It is also possible to specify the path to the repository, e.g. `git:tag:../my-repo`, `git:dynver:../my-repo`. | |""".stripMargin ) b.mkString } private def envVarContent(groups: Seq[EnvVar.EnvVarGroup], onlyRestricted: Boolean): String = { val b = new StringBuilder b.section( """--- |title: Environment variables |sidebar_position: 7 |---""".stripMargin ) b.section( """Scala CLI uses environment variables to configure its behavior. |Below you can find a list of environment variables used and recognized by Scala CLI. | |However, it should by no means be treated as an exhaustive list. |Some tools and libraries Scala CLI integrates with may have their own, which may or may not be listed here. |""".stripMargin ) groups.foreach { group => b.section( s"## ${group.groupName}", group.all .filter(ev => !ev.requiresPower || !onlyRestricted) .map(ev => s" - `${ev.name}`: ${if ev.requiresPower then "⚡ " else ""}${ev.description}") .mkString("\n") ) } b.mkString } def run(options: InternalDocOptions, args: RemainingArgs): Unit = { val scalaCli = new ScalaCliCommands( "scala-cli", ScalaCli.baseRunnerName, ScalaCli.fullRunnerName ) val commands = scalaCli.commands val restrictedCommands = commands.iterator.collect { case s: ScalaCommand[_] if !s.isRestricted && !s.isExperimental => s }.toSeq val allArgs = commands.flatMap(actualHelp(_).args) val nameFormatter = scalaCli.actualDefaultCommand.nameFormatter val allCliOptionsContent = cliOptionsContent(commands, allArgs, nameFormatter) val restrictedCliOptionsContent = cliOptionsContent(restrictedCommands, allArgs, nameFormatter, onlyRestricted = true) val allCommandsContent = commandsContent(commands, onlyRestricted = false) val restrictedCommandsContent = commandsContent(restrictedCommands, onlyRestricted = true) val scalaOptionsReference = optionsReference(restrictedCommands, nameFormatter) val allUsingDirectiveHandlers = usingDirectiveHandlers ++ usingDirectiveWithReqsHandlers val allDirectivesContent = usingContent( allUsingDirectiveHandlers, requireDirectiveHandlers, onlyRestricted = false ) val restrictedDirectivesContent = usingContent( allUsingDirectiveHandlers.filterNot(_.isRestricted), requireDirectiveHandlers.filterNot(_.isRestricted), onlyRestricted = true ) val restrictedDocsDir = os.rel / "scala-command" val allEnvVarsContent = envVarContent(EnvVar.allGroups, onlyRestricted = false) val restrictedEnvVarsContent = envVarContent(Seq(EnvVar.ScalaCli), onlyRestricted = true) if (options.check) { val content = Seq( (os.rel / "cli-options.md") -> allCliOptionsContent, (os.rel / "commands.md") -> allCommandsContent, (os.rel / "directives.md") -> allDirectivesContent, (os.rel / "build-info.md") -> buildInfoContent, (os.rel / "env-vars.md") -> allEnvVarsContent, (os.rel / restrictedDocsDir / "cli-options.md") -> restrictedCliOptionsContent, (os.rel / restrictedDocsDir / "commands.md") -> restrictedCommandsContent, (os.rel / restrictedDocsDir / "directives.md") -> restrictedDirectivesContent, (os.rel / restrictedDocsDir / "runner-specification") -> scalaOptionsReference, (os.rel / restrictedDocsDir / "env-vars.md") -> restrictedEnvVarsContent ) var anyDiff = false for ((dest, content0) <- content) { val dest0 = options.outputPath / dest val diffOpt = maybeDiff(options.outputPath / dest, content0) diffOpt match { case Some(diff) => anyDiff = true System.err.println(Console.RED + prettyPath(dest0) + Console.RESET + " differs:") System.err.println(diff.unifiedDiff) case None => System.err.println( Console.GREEN + prettyPath(dest0) + Console.RESET + " is up-to-date." ) } } if (anyDiff) sys.exit(1) } else { maybeWrite(options.outputPath / "cli-options.md", allCliOptionsContent) maybeWrite(options.outputPath / "commands.md", allCommandsContent) maybeWrite(options.outputPath / "directives.md", allDirectivesContent) maybeWrite(options.outputPath / "build-info.md", buildInfoContent) maybeWrite(options.outputPath / "env-vars.md", allEnvVarsContent) maybeWrite( options.outputPath / restrictedDocsDir / "cli-options.md", restrictedCliOptionsContent ) maybeWrite(options.outputPath / restrictedDocsDir / "commands.md", restrictedCommandsContent) maybeWrite( options.outputPath / restrictedDocsDir / "directives.md", restrictedDirectivesContent ) maybeWrite( options.outputPath / restrictedDocsDir / "runner-specification.md", scalaOptionsReference ) maybeWrite( options.outputPath / restrictedDocsDir / "env-vars.md", restrictedEnvVarsContent ) } } } ================================================ FILE: modules/generate-reference-doc/src/main/scala/scala/cli/doc/InternalDocOptions.scala ================================================ package scala.cli.doc import caseapp.* final case class InternalDocOptions( outputDir: String = "website/docs/reference", check: Boolean = false ) { lazy val outputPath: os.Path = os.Path(outputDir, os.pwd) } object InternalDocOptions { implicit lazy val parser: Parser[InternalDocOptions] = Parser.derive implicit lazy val help: Help[InternalDocOptions] = Help.derive } ================================================ FILE: modules/generate-reference-doc/src/main/scala/scala/cli/doc/ReferenceDocUtils.scala ================================================ package scala.cli.doc import caseapp.HelpMessage import scala.annotation.tailrec import scala.build.internals.ConsoleUtils.* object ReferenceDocUtils { extension (s: String) { def consoleToFence: String = { @tailrec def consoleToFenceRec( remainingLines: Seq[String], fenceOpen: Boolean = false, acc: String = "" ): String = remainingLines.headOption match case None => acc case Some(line) => val openFenceString = if line.contains(Console.BOLD) then """```sh |""".stripMargin else if line.contains(ScalaCliConsole.GRAY) then """```scala |""".stripMargin else "" val currentFenceOpen = fenceOpen || openFenceString.nonEmpty val closeFenceString = if currentFenceOpen && line.contains(Console.RESET) then """ |```""".stripMargin else "" val newFenceOpen = currentFenceOpen && closeFenceString.isEmpty val newLine = s"$openFenceString${line.noConsoleKeys}$closeFenceString" val newAcc = if acc.isEmpty then newLine else s"""$acc |$newLine""".stripMargin consoleToFenceRec(remainingLines.tail, newFenceOpen, newAcc) consoleToFenceRec(s.linesIterator.toSeq) } def filterOutHiddenStrings: String = s .replace(s"${ScalaCliConsole.GRAY}(hidden)${Console.RESET} ", "") .replace(s"${ScalaCliConsole.GRAY}(experimental)${Console.RESET} ", "") .replace(s"${ScalaCliConsole.GRAY}(power)${Console.RESET} ", "") def consoleYellowToMdBullets: String = s.replace(Console.YELLOW, "- ") def consoleToMarkdown: String = s.filterOutHiddenStrings.consoleYellowToMdBullets.consoleToFence } extension (helpMessage: HelpMessage) { def referenceDocMessage: String = helpMessage.message.consoleToMarkdown def referenceDocDetailedMessage: String = { val msg = if helpMessage.detailedMessage.nonEmpty then helpMessage.detailedMessage else helpMessage.message msg.consoleToMarkdown } } } ================================================ FILE: modules/integration/docker/src/test/scala/scala/cli/integration/RunDockerTests.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect import java.io.ByteArrayOutputStream import java.nio.charset.Charset import java.util.concurrent.TimeUnit import scala.concurrent.duration.{Duration, FiniteDuration} class RunDockerTests extends munit.FunSuite { override def munitTimeout: Duration = new FiniteDuration(240, TimeUnit.SECONDS) lazy val imageName = Option(System.getenv("SCALA_CLI_IMAGE")).getOrElse { sys.error("SCALA_CLI_IMAGE not set") } lazy val termOpt = if (System.console() == null) Nil else Seq("-t") lazy val ciOpt = Option(System.getenv("CI")).map(v => Seq("-e", s"CI=$v")).getOrElse(Nil) lazy val slimScalaCliImage = "scala-cli-slim" test("run simple app in in docker") { val fileName = "simple.sc" val message = "Hello" val inputs = TestInputs( os.rel / fileName -> s"""val msg = "$message" |println(msg) |""".stripMargin ) inputs.fromRoot { root => val rawOutput = new ByteArrayOutputStream val cmd = Seq[os.Shellable]( // format: off "docker", "run", "--rm", termOpt, "-v", s"$root:/data", "-w", "/data", ciOpt, imageName, fileName // format: on ) os.proc(cmd).call( cwd = root, stdout = os.ProcessOutput { (b, len) => rawOutput.write(b, 0, len) System.err.write(b, 0, len) }, mergeErrIntoOut = true ) val output = new String(rawOutput.toByteArray, Charset.defaultCharset()) expect(output.linesIterator.toVector.last == message) } } if (!imageName.contains(slimScalaCliImage)) { test("package simple app with native in docker") { val fileName = "simple.sc" val inputs = TestInputs( os.rel / fileName -> """println("Hello")""" ) inputs.fromRoot { root => val cmdPackage = Seq[os.Shellable]( // format: off "docker", "run", "--rm", termOpt, "-v", s"$root:/data", "-w", "/data", ciOpt, imageName, "--power", "package", "--native", fileName, "-o", "Hello" // format: on ) val procPackage = os.proc(cmdPackage).call(cwd = root, check = false) expect(procPackage.exitCode == 0) } } test("package simple app with graalVM in docker") { val fileName = "simple.sc" val inputs = TestInputs( os.rel / fileName -> """println("Hello")""" ) inputs.fromRoot { root => val cmdPackage = Seq[os.Shellable]( // format: off "docker", "run", "--rm", termOpt, "-v", s"$root:/data", "-w", "/data", ciOpt, imageName, "--power", "package", "--native-image", fileName, "-o", "Hello" // format: on ) val procPackage = os.proc(cmdPackage).call(cwd = root, check = false) expect(procPackage.exitCode == 0) } } } } ================================================ FILE: modules/integration/src/main/scala/scala/cli/integration/TestInputs.scala ================================================ package scala.cli.integration import java.io.{FileOutputStream, IOException} import java.nio.charset.{Charset, StandardCharsets} import java.security.SecureRandom import java.util.concurrent.atomic.AtomicInteger import java.util.zip.{ZipEntry, ZipOutputStream} import scala.cli.integration.TestInputs.compress import scala.util.control.NonFatal final case class TestInputs(maybeCharset: Option[Charset], files: (os.RelPath, String)*) { private lazy val charset = maybeCharset.getOrElse(StandardCharsets.UTF_8) def add(extraFiles: (os.RelPath, String)*): TestInputs = TestInputs((files ++ extraFiles)*) private def writeIn(dir: os.Path): Unit = for ((relPath, content) <- files) { val path = dir / relPath os.write(path, content.getBytes(charset), createFolders = true) } def root(): os.Path = { val tmpDir = TestInputs.tmpDir writeIn(tmpDir) tmpDir } def asZip[T](f: (os.Path, os.Path) => T): T = TestInputs.withTmpDir { tmpDir => val zipArchivePath = tmpDir / s"${tmpDir.last}.zip" compress(zipArchivePath, files.map { case (relPath, content) => (relPath, content, charset) }) f(tmpDir, zipArchivePath) } def fromRoot[T](f: os.Path => T): T = TestInputs.withTmpDir { tmpDir => writeIn(tmpDir) f(tmpDir) } def fileNames: Seq[String] = files.flatMap(_._1.lastOpt) } object TestInputs { def apply(files: (os.RelPath, String)*): TestInputs = new TestInputs(None, files*) def apply(charsetName: String, files: (os.RelPath, String)*): TestInputs = { val charset: Charset = Charset.forName(charsetName) new TestInputs(Some(charset), files*) } def empty: TestInputs = TestInputs() def compress(zipFilepath: os.Path, files: Seq[(os.RelPath, String, Charset)]) = { val zip = new ZipOutputStream(new FileOutputStream(zipFilepath.toString())) try for ((relPath, content, charset) <- files) { zip.putNextEntry(new ZipEntry(relPath.toString())) val in: Array[Byte] = content.getBytes(charset) zip.write(in) zip.closeEntry() } finally zip.close() } private lazy val baseTmpDir = { Option(System.getenv("SCALA_CLI_TMP")).getOrElse { sys.error("SCALA_CLI_TMP not set") } val base = os.Path(System.getenv("SCALA_CLI_TMP"), os.pwd) val rng = new SecureRandom val d = base / s"run-${math.abs(rng.nextInt().toLong)}" os.makeDir.all(d) Runtime.getRuntime.addShutdownHook( new Thread("scala-cli-its-clean-up-tmp-dir") { setDaemon(true) override def run(): Unit = try os.remove.all(d) catch { case NonFatal(_) => System.err.println(s"Could not remove $d, ignoring it.") } } ) d } private val tmpCount = new AtomicInteger private def withTmpDir[T](f: os.Path => T): T = { val tmpDir = baseTmpDir / s"test-${tmpCount.incrementAndGet()}" os.makeDir.all(tmpDir) val tmpDir0 = os.Path(tmpDir.toIO.getCanonicalFile) def removeAll(): Unit = try os.remove.all(tmpDir0) catch { case ex: IOException => System.err.println(s"Ignoring $ex while removing $tmpDir0") } try f(tmpDir0) finally removeAll() } private def tmpDir: os.Path = { val tmpDir = baseTmpDir / s"test-${tmpCount.incrementAndGet()}" os.makeDir.all(tmpDir) os.Path(tmpDir.toIO.getCanonicalFile) } } ================================================ FILE: modules/integration/src/test/java/scala/cli/integration/bsp/WrappedSourceItem.java ================================================ package scala.cli.integration.bsp; public class WrappedSourceItem { public String uri; public String generatedUri; public String topWrapper; public String bottomWrapper; } ================================================ FILE: modules/integration/src/test/java/scala/cli/integration/bsp/WrappedSourcesItem.java ================================================ package scala.cli.integration.bsp; import ch.epfl.scala.bsp4j.BuildTargetIdentifier; import java.util.List; public class WrappedSourcesItem { public BuildTargetIdentifier target; public List sources; } ================================================ FILE: modules/integration/src/test/java/scala/cli/integration/bsp/WrappedSourcesResult.java ================================================ package scala.cli.integration.bsp; import java.util.List; public class WrappedSourcesResult { public List items; } ================================================ FILE: modules/integration/src/test/resources/test-keys/key.asc ================================================ -----BEGIN PGP PUBLIC KEY BLOCK----- Version: BCPG v1.68 mQENBGIXxr8DCAC8aS9gAPsdaZukOb9Q83+5+U8IeDdWhOkwPjbIM534BO/LRYVv vKcXcv2X6r5eOnA2NBnAuyZ0GBwiAMVm6agZT2HGY6LjnFlIxn6L/Miz5vlplAXt Q72IybRJUTVEUrV1a4mAxZsM5+mXHmQI7BZ97v9//3uJOlUtXLyjCPA7PwmzQCws npdXb+oOodHTUptsG+8r8Y7XYuFXiuvGq6NRY7aESE9pRupfCAwERRAp7qEddyTo V5xiVSDAn0vxphKd1ZlKGWsK0SH2Rm+QnZZAM5FW0gaGNzp2n6vXtCVSeMs2ZzlE LJCt+ZuxQANuICL9X0YL6hDlq3/co5qSbuxXABEBAAG0Em5vcmVwbHlAZ2l0aHVi LmNvbYkBLgQTAwIAGAUCYhfGvwIbAwQLCQgHBhUIAgkKCwIeAQAKCRA7ulNUJg/z pwFIB/96ntiTdjfr3xzsvCn+iuq3SJBdGALVbIA8htIEfQeafFtFq9fOY26b8efg xO5Foe0gtLkCycviR/ok6xMrTsE2Qcq1clUanAkH5RffAz/jea8fYN150FpANvlN 1Ru3fjX5+FxmPLZa5gj1nPYU+t9uvRphAQxAFaDxbUOgaCfPD03pdLWBhhlG+wrE IvR2aY1JdATgsHqFcpJal6Qs0nNQAX/298OLegcKKEBAllVZ+rwbZ9NEn23X6YP3 wBerflBiV7KDf70R49d99i64V7A2Jh+LiAmyBqRn2E+dUluTX+d1Cv3dia6/dDxL /MQCI5BBKoTFiunGRpolcvu+MKnZuQENBGIXxr8CCADScBUwWHEH8JK2LvPfJRn0 oKpz1xYr1e4Uhh+LangmoESHXfvr+NMtKpMJzhfzKqSREVsQ3iMkk0IqZIKEf2Ay MzBVnbbHQDVUd2c3fu6H0RIJI9zZlaUVAkBL/8c2BvAjqe94wPJkSUjHYalI177X uMmgfpq42gWs3F+W4TmvXkXynOWnNezZWXcJyfODJsraKgBcHJW2Gn86w4EO5jGF KR7zNZfSMU+WZuM4XajDpO8iwRNsu/eYlB8jBPLOlV2jqaOJVgjGwzOKyjfD+9j3 2duOB/da1NWIJi9jzI3tp0PyIAP6fhBuh4Ou5NErasLvU1e/mUAsrYrnLicdBv7x ABEBAAGJAR8EGAMCAAkFAmIXxr8CGwwACgkQO7pTVCYP86f1dQf8DMEG2LkVH6fB qktyVl7cfcHUPa4uqlB2NiHYwrwAr45ftU5/kDBXf9AxoZvoq8pF+y3RMboYu1CU FoxoVh7GB+LtpHk1SyWxZcPo48RoANHkpp4y4XndZcJ4XDbaMkrF65E/CGK7etEX /P+xMyovli64iaXHpIJ9ejKyhSKpFuD7w5U1UzArIfR8xjwrGeonruyiHTB/ULq5 Tp7dU/066ZxlHddXh3WNYhkaD7b3PJP+hCZzn0+LSiA2cKiQDpLX5xm/iAh8CWyq N2Ix77qdIgS7lWd0CEomQ3QV4SYff4u13aUhAer2YZcmFhsejHp2hJD0pvUW41f5 eiL+IwMGMQ== =TGbn -----END PGP PUBLIC KEY BLOCK----- ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/ArgsFileTests.scala ================================================ package scala.cli.integration import scala.util.Properties class ArgsFileTests extends ScalaCliSuite { val forOption = List(true, false) for (useServer <- forOption) test( s"pass scalac options using arguments file ${if (useServer) "with bloop" else "without bloop"}" ) { val fileName = "Simple.sc" val serverArgs = if (useServer) Nil else List("--server=false") val inputs = TestInputs( os.rel / "args.txt" -> """|-release |8""".stripMargin, os.rel / fileName -> s"""| |println("Hello :)".repeat(11)) |""".stripMargin ) inputs.fromRoot { root => val res = os.proc(TestUtil.cli, serverArgs, "@args.txt", fileName).call( cwd = root, check = false, stderr = os.Pipe ) assert(res.exitCode == 1) val compilationError = res.err.text() assert(compilationError.contains("Compilation failed")) } } if (!Properties.isWin) test("pass scalac options using arguments file in shebang script") { val inputs = TestInputs( os.rel / "args.txt" -> """|-release 8""".stripMargin, os.rel / "script-with-shebang" -> s"""|#!/usr/bin/env -S ${TestUtil.cli.mkString(" ")} shebang @args.txt | |println("Hello :)".repeat(11)) |""".stripMargin ) inputs.fromRoot { root => os.perms.set(root / "script-with-shebang", os.PermSet.fromString("rwx------")) val res = os.proc("./script-with-shebang").call(cwd = root, check = false, stderr = os.Pipe) assert(res.exitCode == 1) val compilationError = res.err.text() assert(compilationError.contains("Compilation failed")) } } test("multiple args files") { val preCompileDir = "PreCompileDir" val runDir = "RunDir" val preCompiledInput = "Message.scala" val mainInput = "Main.scala" val expectedOutput = "Hello" val outputDir = os.rel / "out" TestInputs( os.rel / preCompileDir / preCompiledInput -> "case class Message(value: String)", os.rel / runDir / mainInput -> s"""object Main extends App { println(Message("$expectedOutput").value) }""", os.rel / runDir / "args.txt" -> s"""|-d |$outputDir""".stripMargin, os.rel / runDir / "args2.txt" -> s"""|-cp |${os.rel / os.up / preCompileDir / outputDir}""".stripMargin ).fromRoot { (root: os.Path) => os.proc( TestUtil.cli, "compile", "--scala-opt", "-d", "--scala-opt", outputDir.toString, preCompiledInput ).call(cwd = root / preCompileDir, stderr = os.Pipe) assert((root / preCompileDir / outputDir / "Message.class").toNIO.toFile().exists()) val compileOutput = root / runDir / outputDir os.makeDir.all(compileOutput) val runRes = os.proc( TestUtil.cli, "run", "@args.txt", "--server=false", "@args2.txt", mainInput ).call(cwd = root / runDir, stderr = os.Pipe) assert(runRes.out.trim() == expectedOutput) assert((compileOutput / "Main.class").toNIO.toFile().exists()) } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/BloopTests.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect import scala.cli.integration.util.BloopUtil import scala.concurrent.ExecutionContext import scala.concurrent.duration.Duration import scala.util.Properties class BloopTests extends ScalaCliSuite { def runScalaCli(args: String*): os.proc = os.proc(TestUtil.cli, args) private lazy val bloopDaemonDir = BloopUtil.bloopDaemonDir(runScalaCli("--power", "directories").call().out.text()) val dummyInputs: TestInputs = TestInputs( os.rel / "Test.scala" -> """//> using scala 2.13 |object Test { | def main(args: Array[String]): Unit = | println("Hello " + "from test") |} |""".stripMargin ) def testScalaTermination( currentBloopVersion: String, shouldRestart: Boolean ): Unit = TestUtil.retryOnCi() { dummyInputs.fromRoot { root => BloopUtil.killBloop() val bloop = BloopUtil.bloop(currentBloopVersion, bloopDaemonDir) bloop(Seq("about")).call(cwd = root, stdout = os.Inherit) val output = os.proc(TestUtil.cli, "run", ".") .call(cwd = root, stderr = os.Pipe, mergeErrIntoOut = true) .out.text() expect(output.contains("Hello from test")) if (shouldRestart) output.contains("Shutting down unsupported Bloop") else output.contains("No need to restart Bloop") val versionLine = bloop(Seq("about")).call(cwd = root).out.lines()(0) expect(versionLine == "bloop v" + Constants.bloopVersion) } } // Disabled until we have at least 2 Bleep releases // test("scala-cli terminates incompatible bloop") { // testScalaTermination("1.4.8-122-794af022", shouldRestart = true) // } test("scala-cli keeps compatible bloop running") { testScalaTermination(Constants.bloopVersion, shouldRestart = false) } test("invalid bloop options passed via global bloop config json file cause bloop start failure") { val inputs = TestInputs( os.rel / "bloop.json" -> """|{ | "javaOptions" : ["-Xmx1k"] | }""".stripMargin ) inputs.fromRoot { root => runScalaCli("--power", "bloop", "exit").call() val res = runScalaCli( "--power", "bloop", "start", "--bloop-global-options-file", (root / "bloop.json").toString() ).call(cwd = root, stderr = os.Pipe, check = false) expect(res.exitCode == 1) expect(res.err.text().contains("Server failed with exit code 1") || res.err.text().contains( "java.lang.OutOfMemoryError: Garbage-collected heap size exceeded" )) } } test("bloop exit works") { def bloopRunning(): Boolean = { val javaProcesses = os.proc("jps", "-l").call().out.text() javaProcesses.contains("bloop.BloopServer") } val inputs = TestInputs.empty inputs.fromRoot { _ => BloopUtil.killBloop() TestUtil.retry()(assert(!bloopRunning())) val res = runScalaCli("--power", "bloop", "start").call(check = false) assert(res.exitCode == 0, clues(res.out.text())) assert(bloopRunning(), clues(res.out.text())) val resExit = runScalaCli("--power", "bloop", "exit").call(check = false) assert(resExit.exitCode == 0, clues(resExit.out.text())) assert(!bloopRunning()) } } test("bloop projects and bloop compile works") { val inputs = TestInputs( os.rel / "Hello.scala" -> """object Hello { | def main(args: Array[String]): Unit = | println("Hello") |} |""".stripMargin ) inputs.fromRoot { root => os.proc(TestUtil.cli, "compile", ".") .call(cwd = root, stdin = os.Inherit, stdout = os.Inherit) val projRes = os.proc(TestUtil.cli, "--power", "bloop", "projects") .call(cwd = root / Constants.workspaceDirName) val projList = projRes.out.trim().linesIterator.toVector expect(projList.length == 1) val proj = projList.head os.proc(TestUtil.cli, "--power", "bloop", "compile", proj) .call(cwd = root / Constants.workspaceDirName) val failRes = os.proc(TestUtil.cli, "--power", "bloop", "foo") .call(cwd = root / Constants.workspaceDirName, check = false, mergeErrIntoOut = true) val failOutput = failRes.out.text() expect(failRes.exitCode == 4) expect(failOutput.contains("Command not found: foo")) } } if (!Properties.isMac || !TestUtil.isNativeCli || !TestUtil.isCI) // TODO make this pass reliably on Mac CI test("Restart Bloop server while watching") { TestUtil.withThreadPool("bloop-restart-test", 2) { pool => val timeout = Duration("90 seconds") val ec = ExecutionContext.fromExecutorService(pool) def content(message: String) = s"""object Hello { | def main(args: Array[String]): Unit = | println("$message") |} |""".stripMargin val sourcePath = os.rel / "Hello.scala" val inputs = TestInputs( sourcePath -> content("Hello") ) inputs.fromRoot { root => val proc = os.proc(TestUtil.cli, "run", "--power", "--offline", "-w", ".") .spawn(cwd = root) val firstLine = TestUtil.readLine(proc.stdout, ec, timeout) expect(firstLine == "Hello") os.proc(TestUtil.cli, "--power", "bloop", "exit") .call(cwd = root) os.write.over(root / sourcePath, content("Foo")) val secondLine = TestUtil.readLine(proc.stdout, ec, timeout) expect(secondLine == "Foo") proc.destroy() } } } test("run bloop with jvm version if > 17") { val hello = "Hello from Java 21" val inputs = TestInputs( os.rel / "Simple.java" -> s"""|//> using jvm 21 |//> using javacOpt --enable-preview -Xlint:preview --release 21 |//> using javaOpt --enable-preview |//> using mainClass Simple | |void main() { | System.out.println("$hello"); |}""".stripMargin ) inputs.fromRoot { root => // start bloop with jvm 17 runScalaCli("--power", "bloop", "start", "--jvm", "17").call(cwd = root) val res = runScalaCli("Simple.java").call(cwd = root) res.out.text().contains(hello) // shut down bloop so other tests are run on JDK 17 runScalaCli("bloop", "exit", "--power").call(cwd = root) } } for { lang <- List("scala", "java") useDirective <- List(true, false) option <- List("java-home", "jvm") jvm = Constants.allJavaVersions.filter(_ < 23).max } test(s"compiles $lang file with correct jdk version ($jvm) for $option ${ if useDirective then "use directive" else "option" }") { def isScala = lang == "scala" val optionValue = if option == "java-home" then os.Path(os.proc(TestUtil.cs, "java-home", "--jvm", jvm).call().out.trim()).toString() else jvm.toString val directive = if useDirective then s"//> using ${option.replace("-h", "H")} $optionValue\n" else "" val options = if useDirective then Nil else List(s"--$option", optionValue) val content = if isScala then "object Simple { System.out.println(javax.print.attribute.standard.OutputBin.LEFT) }" else """public class Simple { | public static void main(String[] args) { | System.out.println(javax.print.attribute.standard.OutputBin.LEFT); | } |}""".stripMargin val inputs = TestInputs(os.rel / s"Simple.$lang" -> s"$directive$content") inputs.fromRoot { root => val res = runScalaCli("compile" :: "." :: options*).call(root, check = false, stderr = os.Pipe) assert(res.exitCode == 1) val compilationError = res.err.text() val message = if isScala then "value OutputBin is not a member of javax.print.attribute.standard" else "cannot find symbol" assert(compilationError.contains("Compilation failed")) assert(compilationError.contains(message)) } } { val bloopSnapshotVersion = "2.0.6-51-38c118d4-SNAPSHOT" test(s"compilation works with a Bloop snapshot version: $bloopSnapshotVersion".flaky) { val input = "script.sc" TestInputs(os.rel / input -> """println("Hello")""").fromRoot { root => os.proc(TestUtil.cli, "compile", input, "--bloop-version", bloopSnapshotVersion) .call(cwd = root) } } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/BspSuite.scala ================================================ package scala.cli.integration import ch.epfl.scala.bsp4j as b import com.eed3si9n.expecty.Expecty.expect import com.github.plokhotnyuk.jsoniter_scala.core.* import com.github.plokhotnyuk.jsoniter_scala.macros.* import com.google.gson.Gson import com.google.gson.internal.LinkedTreeMap import org.eclipse.lsp4j.jsonrpc.messages.ResponseError import java.net.URI import java.util.concurrent.{ExecutorService, ScheduledExecutorService} import scala.annotation.tailrec import scala.cli.integration.BspSuite.{Details, detailsCodec} import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration.* import scala.concurrent.{Await, Future, Promise} import scala.jdk.CollectionConverters.* import scala.util.control.NonFatal import scala.util.{Failure, Success, Try} trait BspSuite { this: ScalaCliSuite => protected def extraOptions: Seq[String] def initParams(root: os.Path): b.InitializeBuildParams = new b.InitializeBuildParams( "Scala CLI ITs", "0", Constants.bspVersion, root.toNIO.toUri.toASCIIString, new b.BuildClientCapabilities(List("java", "scala").asJava) ) val pool: ExecutorService = TestUtil.threadPool("bsp-tests-jsonrpc", 4) val scheduler: ScheduledExecutorService = TestUtil.scheduler("bsp-tests-scheduler") def completeIn(duration: FiniteDuration): Future[Unit] = { val p = Promise[Unit]() scheduler.schedule( new Runnable { def run(): Unit = try p.success(()) catch { case t: Throwable => System.err.println(s"Caught $t while trying to complete timer, ignoring it") } }, duration.length, duration.unit ) p.future } override def afterAll(): Unit = { pool.shutdown() } protected def extractMainTargets(targets: Seq[b.BuildTargetIdentifier]): b.BuildTargetIdentifier = targets.collectFirst { case t if !t.getUri.contains("-test") => t }.get protected def extractTestTargets(targets: Seq[b.BuildTargetIdentifier]): b.BuildTargetIdentifier = targets.collectFirst { case t if t.getUri.contains("-test") => t }.get def withBsp[T]( inputs: TestInputs, args: Seq[String], attempts: Int = if (TestUtil.isCI) 3 else 1, pauseDuration: FiniteDuration = 5.seconds, bspOptions: List[String] = List.empty, bspEnvs: Map[String, String] = Map.empty, reuseRoot: Option[os.Path] = None, stdErrOpt: Option[os.RelPath] = None, extraOptionsOverride: Seq[String] = extraOptions )( f: ( os.Path, TestBspClient, b.BuildServer & b.ScalaBuildServer & b.JavaBuildServer & b.JvmBuildServer & TestBspClient.WrappedSourcesBuildServer ) => Future[T] ): T = withBspInitResults( inputs, args, attempts, pauseDuration, bspOptions, bspEnvs, reuseRoot, stdErrOpt, extraOptionsOverride )((root, client, server, _: b.InitializeBuildResult) => f(root, client, server)) def withBspInitResults[T]( inputs: TestInputs, args: Seq[String], attempts: Int = if (TestUtil.isCI) 3 else 1, pauseDuration: FiniteDuration = 5.seconds, bspOptions: List[String] = List.empty, bspEnvs: Map[String, String] = Map.empty, reuseRoot: Option[os.Path] = None, stdErrOpt: Option[os.RelPath] = None, extraOptionsOverride: Seq[String] = extraOptions )( f: ( os.Path, TestBspClient, b.BuildServer & b.ScalaBuildServer & b.JavaBuildServer & b.JvmBuildServer & TestBspClient.WrappedSourcesBuildServer, b.InitializeBuildResult ) => Future[T] ): T = { def attempt(): Try[T] = Try { val inputsRoot = inputs.root() val root = reuseRoot.getOrElse(inputsRoot) val stdErrPathOpt: Option[os.ProcessOutput] = stdErrOpt.map(path => root / path) val stderr: os.ProcessOutput = stdErrPathOpt.getOrElse(os.Inherit) val proc = os.proc(TestUtil.cli, "bsp", bspOptions ++ extraOptionsOverride, args) .spawn(cwd = root, stderr = stderr, env = bspEnvs) var remoteServer : b.BuildServer & b.ScalaBuildServer & b.JavaBuildServer & b.JvmBuildServer & TestBspClient.WrappedSourcesBuildServer = null val bspServerExited = Promise[Unit]() val t = new Thread("bsp-server-watcher") { setDaemon(true) override def run() = { proc.join() bspServerExited.success(()) } } t.start() def whileBspServerIsRunning[T](f: Future[T]): Future[T] = { val ex = new Exception Future.firstCompletedOf(Seq(f.map(Right(_)), bspServerExited.future.map(Left(_)))) .transform { case Success(Right(t)) => Success(t) case Success(Left(())) => Failure(new Exception("BSP server exited too early", ex)) case Failure(ex) => Failure(ex) } } try { val (localClient, remoteServer0, _) = TestBspClient.connect(proc.stdout, proc.stdin, pool) remoteServer = remoteServer0 val initRes: b.InitializeBuildResult = Await.result( whileBspServerIsRunning(remoteServer.buildInitialize(initParams(root)).asScala), Duration.Inf ) Await.result( whileBspServerIsRunning(f(root, localClient, remoteServer, initRes)), Duration.Inf ) } finally { if (remoteServer != null) try Await.result(whileBspServerIsRunning(remoteServer.buildShutdown().asScala), 20.seconds) catch { case NonFatal(e) => System.err.println(s"Ignoring $e while shutting down BSP server") } proc.join(2.seconds.toMillis) proc.destroy() proc.join(2.seconds.toMillis) proc.destroy(shutdownGracePeriod = 0) } } @tailrec def helper(count: Int): T = attempt() match { case Success(t) => t case Failure(ex) => if (count <= 1) throw new Exception(ex) else { System.err.println(s"Caught $ex, trying again in $pauseDuration…") Thread.sleep(pauseDuration.toMillis) helper(count - 1) } } helper(attempts) } def checkTargetUri(root: os.Path, uri: String): Unit = { val baseUri = TestUtil.normalizeUri((root / Constants.workspaceDirName).toNIO.toUri.toASCIIString) .stripSuffix("/") val expectedPrefixes = Set( baseUri + "?id=", baseUri + "/?id=" ) expect(expectedPrefixes.exists(uri.startsWith)) } protected def readBspConfig( root: os.Path, connectionJsonFileName: String = "scala-cli.json" ): Details = { val bspFile = root / ".bsp" / connectionJsonFileName expect(os.isFile(bspFile)) val content = os.read.bytes(bspFile) // check that we can decode the connection details readFromArray(content)(detailsCodec) } protected def checkIfBloopProjectIsInitialised( root: os.Path, buildTargetsResp: b.WorkspaceBuildTargetsResult ): Unit = { val targets = buildTargetsResp.getTargets.asScala.map(_.getId).toSeq expect(targets.length == 2) val bloopProjectNames = targets.map { target => val targetUri = TestUtil.normalizeUri(target.getUri) checkTargetUri(root, targetUri) new URI(targetUri).getQuery.stripPrefix("id=") } val bloopDir = root / Constants.workspaceDirName / ".bloop" expect(os.isDir(bloopDir)) bloopProjectNames.foreach { bloopProjectName => val bloopProjectJsonPath = bloopDir / s"$bloopProjectName.json" expect(os.isFile(bloopProjectJsonPath)) } } protected def extractDiagnosticsParams( relevantFilePath: os.Path, localClient: TestBspClient ): b.PublishDiagnosticsParams = { val params = localClient.latestDiagnostics().getOrElse { sys.error("No diagnostics found") } expect { TestUtil.normalizeUri(params.getTextDocument.getUri) == TestUtil.normalizeUri( relevantFilePath.toNIO.toUri.toASCIIString ) } params } protected def checkDiagnostic( diagnostic: b.Diagnostic, expectedMessage: String, expectedSeverity: b.DiagnosticSeverity, expectedStartLine: Int, expectedStartCharacter: Int, expectedEndLine: Int, expectedEndCharacter: Int, expectedSource: Option[String] = None, strictlyCheckMessage: Boolean = true ): Unit = { expect(diagnostic.getSeverity == expectedSeverity) expect(diagnostic.getRange.getStart.getLine == expectedStartLine) expect(diagnostic.getRange.getStart.getCharacter == expectedStartCharacter) expect(diagnostic.getRange.getEnd.getLine == expectedEndLine) expect(diagnostic.getRange.getEnd.getCharacter == expectedEndCharacter) val message = TestUtil.removeAnsiColors(diagnostic.getMessage) if (strictlyCheckMessage) assertNoDiff(message, expectedMessage) else expect(message.contains(expectedMessage)) for (es <- expectedSource) expect(diagnostic.getSource == es) } protected def checkScalaAction( diagnostic: b.Diagnostic, expectedActionsSize: Int, expectedTitle: String, expectedChanges: Int, expectedStartLine: Int, expectedStartCharacter: Int, expectedEndLine: Int, expectedEndCharacter: Int, expectedNewText: String ): Unit = { expect(diagnostic.getDataKind == "scala") val gson = new com.google.gson.Gson() val scalaDiagnostic: b.ScalaDiagnostic = gson.fromJson( diagnostic.getData.toString, classOf[b.ScalaDiagnostic] ) val actions = scalaDiagnostic.getActions.asScala expect(actions.size == expectedActionsSize) val action = actions.head expect(action.getTitle == expectedTitle) val edit = action.getEdit expect(edit.getChanges.asScala.size == expectedChanges) val change = edit.getChanges.asScala.head val expectedRange = new b.Range( new b.Position(expectedStartLine, expectedStartCharacter), new b.Position(expectedEndLine, expectedEndCharacter) ) expect(change.getRange == expectedRange) expect(change.getNewText == expectedNewText) } protected def extractWorkspaceReloadResponse(workspaceReloadResult: AnyRef) : Option[ResponseError] = workspaceReloadResult match { case gsonMap: LinkedTreeMap[?, ?] if !gsonMap.isEmpty => val gson = new Gson() Some(gson.fromJson(gson.toJson(gsonMap), classOf[ResponseError])) case _ => None } } object BspSuite { final protected case class Details( name: String, version: String, bspVersion: String, argv: List[String], languages: List[String] ) protected val detailsCodec: JsonValueCodec[Details] = JsonCodecMaker.make } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/BspTestDefinitions.scala ================================================ package scala.cli.integration import ch.epfl.scala.bsp4j as b import ch.epfl.scala.bsp4j.JvmTestEnvironmentParams import com.eed3si9n.expecty.Expecty.expect import com.google.gson.{Gson, JsonElement} import java.net.URI import java.nio.file.Paths import scala.cli.integration.TestUtil.await import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future import scala.concurrent.duration.* import scala.jdk.CollectionConverters.* import scala.util.Properties abstract class BspTestDefinitions extends ScalaCliSuite with TestScalaVersionArgs with BspSuite with ScriptWrapperTestDefinitions with CoursierScalaInstallationTestHelper { this: TestScalaVersion => protected lazy val extraOptions: Seq[String] = scalaVersionArgs ++ TestUtil.extraOptions test("setup-ide") { val inputs = TestInputs( os.rel / "simple.sc" -> s"""val msg = "Hello" |println(msg) |""".stripMargin ) inputs.fromRoot { root => os.proc(TestUtil.cli, "setup-ide", ".", extraOptions).call(cwd = root, stdout = os.Inherit) val details = readBspConfig(root) expect(details.argv.length >= 2) expect(details.argv.dropWhile(_ != TestUtil.cliPath).drop(1).head == "bsp") } } for (command <- Seq("setup-ide", "compile", "run")) test(command + " should result in generated bsp file") { val inputs = TestInputs( os.rel / "simple.sc" -> s"""val msg = "Hello" |println(msg) |""".stripMargin ) inputs.fromRoot { root => os.proc(TestUtil.cli, "--power", command, ".", extraOptions) .call(cwd = root, stdout = os.Inherit) val details = readBspConfig(root) val expectedIdeOptionsFile = root / Constants.workspaceDirName / "ide-options-v2.json" val expectedIdeLaunchFile = root / Constants.workspaceDirName / "ide-launcher-options.json" val expectedIdeInputsFile = root / Constants.workspaceDirName / "ide-inputs.json" val expectedIdeEnvsFile = root / Constants.workspaceDirName / "ide-envs.json" val expectedArgv = Seq( TestUtil.cliPath, "--power", "bsp", "--json-options", expectedIdeOptionsFile.toString, "--json-launcher-options", expectedIdeLaunchFile.toString, "--envs-file", expectedIdeEnvsFile.toString, root.toString ) if (TestUtil.isJvmBootstrappedCli) { expect(details.argv.head.endsWith("java")) expect(details.argv.drop(1).head == "-jar") } expect(details.argv.dropWhile(_ != TestUtil.cliPath) == expectedArgv) expect(os.isFile(expectedIdeOptionsFile)) expect(os.isFile(expectedIdeInputsFile)) expect(os.isFile(expectedIdeEnvsFile)) } } val importPprintOnlyProject: TestInputs = TestInputs( os.rel / "simple.sc" -> s"//> using dep \"com.lihaoyi::pprint:${Constants.pprintVersion}\"" ) test("setup-ide should have only absolute paths even if relative ones were specified") { val path = os.rel / "directory" / "simple.sc" val inputs = TestInputs(path -> s"//> using dep \"com.lihaoyi::pprint:${Constants.pprintVersion}\"") inputs.fromRoot { root => val relativeCliCommand = TestUtil.cliCommand( TestUtil.relPathStr(os.Path(TestUtil.cliPath).relativeTo(root)) ) val proc = if (Properties.isWin && TestUtil.isNativeCli) os.proc( "cmd", "/c", (relativeCliCommand ++ Seq("--power", "setup-ide", path.toString) ++ extraOptions) .mkString(" ") ) else os.proc(relativeCliCommand, "--power", "setup-ide", path, extraOptions) proc.call(cwd = root, stdout = os.Inherit) val details = readBspConfig(root / "directory") val expectedArgv = List( TestUtil.cliPath, "--power", "bsp", "--json-options", (root / "directory" / Constants.workspaceDirName / "ide-options-v2.json").toString, "--json-launcher-options", (root / "directory" / Constants.workspaceDirName / "ide-launcher-options.json").toString, "--envs-file", (root / "directory" / Constants.workspaceDirName / "ide-envs.json").toString, (root / "directory" / "simple.sc").toString ) expect(details.argv.dropWhile(_ != TestUtil.cliPath) == expectedArgv) } } test("setup-ide should succeed for valid dependencies") { importPprintOnlyProject.fromRoot { root => os.proc( TestUtil.cli, "setup-ide", ".", extraOptions, "--dependency", s"org.scalameta::munit:${Constants.munitVersion}" ).call(cwd = root) } } test("setup-ide should fail for invalid dependencies") { importPprintOnlyProject.fromRoot { root => val p = os.proc( TestUtil.cli, "setup-ide", ".", extraOptions, "--dependency", "org.scalameta::munit:0.7.119999" ).call(cwd = root, check = false, stderr = os.Pipe) expect(p.err.text().contains(s"Error downloading org.scalameta:munit")) expect(p.exitCode == 1) } } test("simple") { val inputs = TestInputs( os.rel / "simple.sc" -> s"""val msg = "Hello" |println(msg) |""".stripMargin ) withBsp(inputs, Seq(".")) { (root, _, remoteServer) => Future { val buildTargetsResp = remoteServer.workspaceBuildTargets().asScala.await val target = { val targets = buildTargetsResp.getTargets.asScala.map(_.getId).toSeq expect(targets.length == 2) extractMainTargets(targets) } val targetUri = TestUtil.normalizeUri(target.getUri) checkTargetUri(root, targetUri) val targets = List(target).asJava { val resp = remoteServer .buildTargetDependencySources(new b.DependencySourcesParams(targets)) .asScala .await val foundTargets = resp.getItems.asScala.map(_.getTarget.getUri).toSeq expect(foundTargets == Seq(targetUri)) val foundDepSources = resp.getItems.asScala .flatMap(_.getSources.asScala) .toSeq .map { uri => val idx = uri.lastIndexOf('/') uri.drop(idx + 1) } if (actualScalaVersion.startsWith("2.")) { expect(foundDepSources.length == 1) expect(foundDepSources.forall(_.startsWith("scala-library-"))) } else { expect(foundDepSources.length == 2) expect(foundDepSources.exists(_.startsWith("scala-library-"))) expect(foundDepSources.exists(_.startsWith("scala3-library_3-3"))) } expect(foundDepSources.forall(_.endsWith("-sources.jar"))) } { val resp = remoteServer.buildTargetSources(new b.SourcesParams(targets)).asScala.await val foundTargets = resp.getItems.asScala.map(_.getTarget.getUri).toSeq expect(foundTargets == Seq(targetUri)) val foundSources = resp.getItems.asScala .map(_.getSources.asScala.map(_.getUri).toSeq) .toSeq .map(_.map(TestUtil.normalizeUri)) val expectedSources = Seq( Seq( TestUtil.normalizeUri((root / "simple.sc").toNIO.toUri.toASCIIString) ) ) expect(foundSources == expectedSources) } val scalacOptionsResp = { val resp = remoteServer .buildTargetScalacOptions(new b.ScalacOptionsParams(targets)) .asScala .await val foundTargets = resp .getItems .asScala .map(_.getTarget.getUri) .map(TestUtil.normalizeUri) expect(foundTargets == Seq(targetUri)) val foundOptions = resp.getItems.asScala.flatMap(_.getOptions.asScala).toSeq if (actualScalaVersion.startsWith("2.")) expect(foundOptions.exists { opt => opt.startsWith("-Xplugin:") && opt.contains("semanticdb-scalac") }) else expect(foundOptions.contains("-Xsemanticdb")) resp } { val resp = remoteServer.buildTargetJavacOptions(new b.JavacOptionsParams(targets)) .asScala.await val foundTargets = resp .getItems .asScala .map(_.getTarget.getUri) .map(TestUtil.normalizeUri) expect(foundTargets == Seq(targetUri)) } val classDir = os.Path( Paths.get(new URI(scalacOptionsResp.getItems.asScala.head.getClassDirectory)) ) { val resp = remoteServer.buildTargetCompile(new b.CompileParams(targets)).asScala.await expect(resp.getStatusCode == b.StatusCode.OK) } val compileProducts = os.walk(classDir).filter(os.isFile(_)).map(_.relativeTo(classDir)) val classFilePath = os.rel / { if actualScalaVersion.startsWith("3.") then "simple$_.class" else "simple$.class" } expect(compileProducts.contains(classFilePath)) val relPath = os.rel / "META-INF" / "semanticdb" / "simple.sc.semanticdb" expect(compileProducts.contains(relPath)) } } } test("diagnostics") { val inputs = TestInputs( os.rel / "Test.scala" -> s"""object Test { | val msg = "Hello" | zz | println(msg) |} |""".stripMargin ) withBsp(inputs, Seq(".")) { (root, localClient, remoteServer) => Future { val buildTargetsResp = remoteServer.workspaceBuildTargets().asScala.await val target = { val targets = buildTargetsResp.getTargets.asScala.map(_.getId).toSeq expect(targets.length == 2) extractMainTargets(targets) } val targetUri = TestUtil.normalizeUri(target.getUri) checkTargetUri(root, targetUri) val targets = List(target).asJava val compileResp = remoteServer.buildTargetCompile(new b.CompileParams(targets)).asScala.await expect(compileResp.getStatusCode == b.StatusCode.ERROR) val diagnosticsParams: b.PublishDiagnosticsParams = extractDiagnosticsParams(root / "Test.scala", localClient) expect(diagnosticsParams.getBuildTarget.getUri == targetUri) val diagnostics = diagnosticsParams.getDiagnostics.asScala.toSeq expect(diagnostics.length == 1) val (expectedMessage, expectedEndCharacter) = if (actualScalaVersion.startsWith("2.")) "not found: value zz" -> 4 else if (actualScalaVersion == "3.0.0") "Not found: zz" -> 2 else "Not found: zz" -> 4 checkDiagnostic( diagnostic = diagnostics.head, expectedMessage = expectedMessage, expectedSeverity = b.DiagnosticSeverity.ERROR, expectedStartLine = 2, expectedStartCharacter = 2, expectedEndLine = 2, expectedEndCharacter = expectedEndCharacter ) } } } test("diagnostics in script") { val inputs = TestInputs( os.rel / "test.sc" -> """val msg: NonExistent = "Hello"""" ) withBsp(inputs, Seq(".")) { (root, localClient, remoteServer) => Future { val buildTargetsResp = remoteServer.workspaceBuildTargets().asScala.await val target = { val targets = buildTargetsResp.getTargets.asScala.map(_.getId).toSeq expect(targets.length == 2) extractMainTargets(targets) } val targetUri = TestUtil.normalizeUri(target.getUri) checkTargetUri(root, targetUri) val targets = List(target).asJava val compileResp = remoteServer .buildTargetCompile(new b.CompileParams(targets)) .asScala .await val actualStatusCode = compileResp.getStatusCode expect(actualStatusCode == b.StatusCode.ERROR) val diagnosticsParams = { val diagnostics = localClient.diagnostics() val params = diagnostics(2) expect(params.getBuildTarget.getUri == targetUri) val actualUri = TestUtil.normalizeUri(params.getTextDocument.getUri) val expectedUri = TestUtil.normalizeUri((root / "test.sc").toNIO.toUri.toASCIIString) expect(actualUri == expectedUri) params } val diagnostics = diagnosticsParams.getDiagnostics.asScala.toSeq expect(diagnostics.length == 1) val expectedMessage = if (actualScalaVersion.startsWith("2.")) "not found: type NonExistent" else "Not found: type NonExistent" checkDiagnostic( diagnostic = diagnostics.head, expectedMessage = expectedMessage, expectedSeverity = b.DiagnosticSeverity.ERROR, expectedStartLine = 0, expectedStartCharacter = 9, expectedEndLine = 0, expectedEndCharacter = 20 ) } } } test("invalid diagnostics at startup") { val inputs = TestInputs( os.rel / "A.scala" -> s"""//> using resource ./resources | |object A {} |""".stripMargin ) withBsp(inputs, Seq(".")) { (_, localClient, remoteServer) => Future { remoteServer.workspaceBuildTargets().asScala.await val diagnosticsParams = localClient.latestDiagnostics().getOrElse { fail("No diagnostics found") } checkDiagnostic( diagnostic = diagnosticsParams.getDiagnostics.asScala.toSeq.head, expectedMessage = "Unrecognized directive: resource with values: ./resources", expectedSeverity = b.DiagnosticSeverity.ERROR, expectedStartLine = 0, expectedStartCharacter = 10, expectedEndLine = 0, expectedEndCharacter = 18, strictlyCheckMessage = false ) } } } test("directive diagnostics") { val inputs = TestInputs( os.rel / "Test.scala" -> s"""//> using dep com.lihaoyi::pprint:0.0.0.0.0.1 | |object Test { | val msg = "Hello" | println(msg) |} |""".stripMargin ) withBsp(inputs, Seq(".")) { (root, localClient, remoteServer) => Future { remoteServer.workspaceBuildTargets().asScala.await val diagnosticsParams = extractDiagnosticsParams(root / "Test.scala", localClient) val diagnostics = diagnosticsParams.getDiagnostics.asScala.toSeq expect(diagnostics.length == 1) val sbv = if (actualScalaVersion.startsWith("2.12.")) "2.12" else if (actualScalaVersion.startsWith("2.13.")) "2.13" else if (actualScalaVersion.startsWith("3.")) "3" else ??? val expectedMessage = s"Error downloading com.lihaoyi:pprint_$sbv:0.0.0.0.0.1" checkDiagnostic( diagnostic = diagnostics.head, expectedMessage = expectedMessage, expectedSeverity = b.DiagnosticSeverity.ERROR, expectedStartLine = 0, expectedStartCharacter = 14, expectedEndLine = 0, expectedEndCharacter = 45, strictlyCheckMessage = false ) } } } test("directives in multiple files diagnostics") { val javaVersion = Constants.allJavaVersions.filter(_ > Constants.defaultJvmVersion).min val inputs = TestInputs( os.rel / "Foo.scala" -> s"""//> using scala $actualScalaVersion | |object Foo extends App { | println("Foo") |} |""".stripMargin, os.rel / "Bar.scala" -> "", os.rel / "Hello.java" -> s"//> using jvm $javaVersion" ) withBsp(inputs, Seq(".")) { (root, localClient, remoteServer) => Future { remoteServer.workspaceBuildTargets().asScala.await val buildTargetsResp = remoteServer.workspaceBuildTargets().asScala.await val target = { val targets = buildTargetsResp.getTargets.asScala.map(_.getId).toSeq expect(targets.length == 2) extractMainTargets(targets) } val targetUri = TestUtil.normalizeUri(target.getUri) checkTargetUri(root, targetUri) val targets = List(target).asJava val compileResp = remoteServer .buildTargetCompile(new b.CompileParams(targets)) .asScala .await expect(compileResp.getStatusCode == b.StatusCode.OK) def checkDirectivesInMultipleFilesWarnings( fileName: String, expectedStartLine: Int, expectedStartCharacter: Int, expectedEndLine: Int, expectedEndCharacter: Int ): Unit = { val diagnosticsParams = localClient.diagnostics().collectFirst { case diag if !diag.getDiagnostics.isEmpty && TestUtil.normalizeUri(diag.getTextDocument.getUri) == TestUtil.normalizeUri((root / fileName).toNIO.toUri.toASCIIString) => diag } expect(diagnosticsParams.isDefined) val diagnostics = diagnosticsParams.get.getDiagnostics.asScala.toSeq val expectedMessage = "Using directives detected in multiple files. It is recommended to keep them centralized in the" checkDiagnostic( diagnostic = diagnostics.head, expectedMessage = expectedMessage, expectedSeverity = b.DiagnosticSeverity.WARNING, expectedStartLine = expectedStartLine, expectedStartCharacter = expectedStartCharacter, expectedEndLine = expectedEndLine, expectedEndCharacter = expectedEndCharacter, strictlyCheckMessage = false ) } checkDirectivesInMultipleFilesWarnings( fileName = "Foo.scala", expectedStartLine = 0, expectedStartCharacter = 0, expectedEndLine = 0, expectedEndCharacter = 16 + actualScalaVersion.length ) checkDirectivesInMultipleFilesWarnings( fileName = "Hello.java", expectedStartLine = 0, expectedStartCharacter = 0, expectedEndLine = 0, expectedEndCharacter = 16 ) } } } test("workspace update") { val inputs = TestInputs( os.rel / "simple.sc" -> s"""val msg = "Hello" |println(msg) |""".stripMargin ) val extraArgs = if (Properties.isWin) Seq("-v", "-v", "-v") else Nil withBsp(inputs, Seq(".") ++ extraArgs) { (root, localClient, remoteServer) => Future { val buildTargetsResp = remoteServer.workspaceBuildTargets().asScala.await val target = { val targets = buildTargetsResp.getTargets.asScala.map(_.getId).toSeq expect(targets.length == 2) extractMainTargets(targets) } val targetUri = TestUtil.normalizeUri(target.getUri) checkTargetUri(root, targetUri) val targets = List(target).asJava { val resp = remoteServer.buildTargetCompile(new b.CompileParams(targets)).asScala.await expect(resp.getStatusCode == b.StatusCode.OK) } { val resp = remoteServer .buildTargetDependencySources(new b.DependencySourcesParams(targets)) .asScala .await val foundTargets = resp.getItems.asScala.map(_.getTarget.getUri).toSeq expect(foundTargets == Seq(targetUri)) val foundDepSources = resp.getItems.asScala .flatMap(_.getSources.asScala) .toSeq .map { uri => val idx = uri.lastIndexOf('/') uri.drop(idx + 1) } if (actualScalaVersion.startsWith("2.")) { expect(foundDepSources.length == 1) expect(foundDepSources.forall(_.startsWith("scala-library-"))) } else { expect(foundDepSources.length == 2) expect(foundDepSources.exists(_.startsWith("scala-library-"))) expect(foundDepSources.exists(_.startsWith("scala3-library_3-3"))) } expect(foundDepSources.forall(_.endsWith("-sources.jar"))) } val didChangeParamsFuture = localClient.buildTargetDidChange() val updatedContent = """//> using dep com.lihaoyi::pprint:0.6.6 |val msg = "Hello" |pprint.log(msg) |""".stripMargin os.write.over(root / "simple.sc", updatedContent) { val resp = remoteServer.buildTargetCompile(new b.CompileParams(targets)).asScala.await expect(resp.getStatusCode == b.StatusCode.OK) } val didChangeParamsOptFuture = Future.firstCompletedOf(Seq( didChangeParamsFuture.map(Some(_)), completeIn(5.seconds).map(_ => None) )) val didChangeParams = didChangeParamsOptFuture.await.getOrElse { sys.error("No buildTargetDidChange notification received") } val changes = didChangeParams.getChanges.asScala.toSeq expect(changes.length == 2) val change = changes.head expect(change.getTarget.getUri == targetUri) val expectedKind = b.BuildTargetEventKind.CHANGED expect(change.getKind == expectedKind) { val resp = remoteServer .buildTargetDependencySources(new b.DependencySourcesParams(targets)) .asScala .await val foundTargets = resp.getItems.asScala.map(_.getTarget.getUri).toSeq expect(foundTargets == Seq(targetUri)) val foundDepSources = resp.getItems.asScala .flatMap(_.getSources.asScala) .toSeq .map { uri => val idx = uri.lastIndexOf('/') uri.drop(idx + 1) } expect(foundDepSources.length > 1) expect(foundDepSources.forall(_.endsWith("-sources.jar"))) expect(foundDepSources.exists(_.startsWith("scala-library-"))) expect(foundDepSources.exists(_.startsWith("pprint_"))) resp } } } } test("workspace update - new file") { val inputs = TestInputs( os.rel / "Test.scala" -> s"""object Test { | val msg = "Hello" | println(msg) |} |""".stripMargin ) withBsp(inputs, Seq(".")) { (root, localClient, remoteServer) => Future { val buildTargetsResp = remoteServer.workspaceBuildTargets().asScala.await val target = { val targets = buildTargetsResp.getTargets.asScala.map(_.getId).toSeq expect(targets.length == 2) extractMainTargets(targets) } val targetUri = TestUtil.normalizeUri(target.getUri) checkTargetUri(root, targetUri) val targets = List(target).asJava { val resp = remoteServer.buildTargetCompile(new b.CompileParams(targets)).asScala.await expect(resp.getStatusCode == b.StatusCode.OK) } { val resp = remoteServer .buildTargetDependencySources(new b.DependencySourcesParams(targets)) .asScala .await val foundTargets = resp.getItems.asScala.map(_.getTarget.getUri).toSeq expect(foundTargets == Seq(targetUri)) val foundDepSources = resp.getItems.asScala .flatMap(_.getSources.asScala) .toSeq .map { uri => val idx = uri.lastIndexOf('/') uri.drop(idx + 1) } if (actualScalaVersion.startsWith("2.")) { expect(foundDepSources.length == 1) expect(foundDepSources.forall(_.startsWith("scala-library-"))) } else { expect(foundDepSources.length == 2) expect(foundDepSources.exists(_.startsWith("scala-library-"))) expect(foundDepSources.exists(_.startsWith("scala3-library_3-3"))) } expect(foundDepSources.forall(_.endsWith("-sources.jar"))) } val didChangeParamsFuture = localClient.buildTargetDidChange() val newFileContent = """object Messages { | def msg = "Hello" |} |""".stripMargin os.write(root / "Messages.scala", newFileContent) val updatedContent = """object Test { | println(Messages.msg) |} |""".stripMargin os.write.over(root / "Test.scala", updatedContent) { val resp = remoteServer.buildTargetCompile(new b.CompileParams(targets)).asScala.await expect(resp.getStatusCode == b.StatusCode.OK) } val didChangeParamsOptFuture = Future.firstCompletedOf(Seq( didChangeParamsFuture.map(Some(_)), completeIn(5.seconds).map(_ => None) )) val didChangeParams = didChangeParamsOptFuture.await.getOrElse { sys.error("No buildTargetDidChange notification received") } val changes = didChangeParams.getChanges.asScala.toSeq expect(changes.length == 2) val change = changes.head expect(change.getTarget.getUri == targetUri) val expectedKind = b.BuildTargetEventKind.CHANGED expect(change.getKind == expectedKind) } } } test("test workspace update after adding file to main scope") { val inputs = TestInputs( os.rel / "Messages.scala" -> """//> using dep com.lihaoyi::os-lib:0.7.8 |object Messages { | def msg = "Hello" |} |""".stripMargin, os.rel / "MyTests.test.scala" -> """//> using dep com.lihaoyi::utest::0.7.10 |import utest._ | |object MyTests extends TestSuite { | val tests = Tests { | test("foo") { | assert(Messages.msg == "Hello") | } | } |} |""".stripMargin ) val actualScalaMajorVersion = actualScalaVersion.split("\\.") .take(if (actualScalaVersion.startsWith("3")) 1 else 2) .mkString(".") withBsp(inputs, Seq(".")) { (root, localClient, remoteServer) => Future { val buildTargetsResp = remoteServer.workspaceBuildTargets().asScala.await val target = { val targets = buildTargetsResp.getTargets.asScala.map(_.getId).toSeq expect(targets.length == 2) extractTestTargets(targets) } val targetUri = TestUtil.normalizeUri(target.getUri) checkTargetUri(root, targetUri) val targets = List(target).asJava { val resp = remoteServer.buildTargetCompile(new b.CompileParams(targets)).asScala.await expect(resp.getStatusCode == b.StatusCode.OK) } { val resp = remoteServer .buildTargetDependencySources(new b.DependencySourcesParams(targets)) .asScala .await val foundTargets = resp.getItems.asScala.map(_.getTarget.getUri).toSeq expect(foundTargets == Seq(targetUri)) val foundDepSources = resp.getItems.asScala .flatMap(_.getSources.asScala) .toSeq .map { uri => val idx = uri.lastIndexOf('/') uri.drop(idx + 1) } expect(foundDepSources.exists(_.startsWith(s"utest_$actualScalaMajorVersion-0.7.10"))) expect(foundDepSources.exists(_.startsWith(s"os-lib_$actualScalaMajorVersion-0.7.8"))) expect(foundDepSources.exists(_.startsWith("test-interface-1.0"))) expect(foundDepSources.forall(_.endsWith("-sources.jar"))) } val changeFuture = localClient.buildTargetDidChange() val newFileContent = """object Messages { | def msg = "Hello2" |} |""".stripMargin os.write.over(root / "Messages.scala", newFileContent) { val resp = remoteServer.buildTargetCompile(new b.CompileParams(targets)).asScala.await expect(resp.getStatusCode == b.StatusCode.OK) } expect(changeFuture.isCompleted) { val resp = remoteServer .buildTargetDependencySources(new b.DependencySourcesParams(targets)) .asScala .await val foundTargets = resp.getItems.asScala.map(_.getTarget.getUri).toSeq expect(foundTargets == Seq(targetUri)) val foundDepSources = resp.getItems.asScala .flatMap(_.getSources.asScala) .toSeq .map { uri => val idx = uri.lastIndexOf('/') uri.drop(idx + 1) } expect(foundDepSources.exists(_.startsWith(s"utest_$actualScalaMajorVersion-0.7.10"))) expect(!foundDepSources.exists(_.startsWith(s"os-lib_$actualScalaMajorVersion-0.7.8"))) expect(foundDepSources.exists(_.startsWith("test-interface-1.0"))) expect(foundDepSources.forall(_.endsWith("-sources.jar"))) } } } } test("return .scala-build directory as a output paths") { val inputs = TestInputs( os.rel / "Hello.scala" -> """object Hello extends App { | println("Hello World") |} |""".stripMargin ) withBspInitResults(inputs, Seq(".")) { (root, _, remoteServer, buildInitRes) => val serverCapabilities: b.BuildServerCapabilities = buildInitRes.getCapabilities expect(serverCapabilities.getOutputPathsProvider) Future { val buildTargetsResp = remoteServer.workspaceBuildTargets().asScala.await val target = { val targets = buildTargetsResp.getTargets.asScala.map(_.getId).toSeq extractTestTargets(targets) } val resp = remoteServer.buildTargetOutputPaths(new b.OutputPathsParams(List(target).asJava)) .asScala.await val outputPathsItems = resp.getItems.asScala assert(outputPathsItems.nonEmpty) val outputPathItem = outputPathsItems.head val expectedOutputPathUri = (root / Constants.workspaceDirName).toIO.toURI.toASCIIString val expectedOutputPathItem = new b.OutputPathsItem( target, List(new b.OutputPathItem(expectedOutputPathUri, b.OutputPathItemKind.DIRECTORY)).asJava ) expect(outputPathItem == expectedOutputPathItem) } } } test("workspace/reload --dependency option") { val inputs = TestInputs( os.rel / "ReloadTest.scala" -> s"""object ReloadTest { | println(os.pwd) |} |""".stripMargin ) inputs.fromRoot { root => os.proc(TestUtil.cli, "setup-ide", ".", extraOptions) .call( cwd = root, stdout = os.Inherit ) val ideOptionsPath = root / Constants.workspaceDirName / "ide-options-v2.json" val jsonOptions = List("--json-options", ideOptionsPath.toString) withBsp(inputs, Seq("."), bspOptions = jsonOptions, reuseRoot = Some(root)) { (_, _, remoteServer) => Future { val buildTargetsResp = remoteServer.workspaceBuildTargets().asScala.await val targets = buildTargetsResp.getTargets.asScala.map(_.getId).toSeq val resp = remoteServer.buildTargetCompile(new b.CompileParams(targets.asJava)) .asScala.await expect(resp.getStatusCode == b.StatusCode.ERROR) val dependencyOptions = List("--dependency", "com.lihaoyi::os-lib::0.8.0") os.proc(TestUtil.cli, "setup-ide", ".", dependencyOptions ++ extraOptions) .call( cwd = root, stdout = os.Inherit ) val reloadResponse = extractWorkspaceReloadResponse(remoteServer.workspaceReload().asScala.await) expect(reloadResponse.isEmpty) val buildTargetsResp0 = remoteServer.workspaceBuildTargets().asScala.await val targets0 = buildTargetsResp0.getTargets.asScala.map(_.getId).toSeq val resp0 = remoteServer.buildTargetCompile(new b.CompileParams(targets0.asJava)) .asScala.await expect(resp0.getStatusCode == b.StatusCode.OK) } } } } test("workspace/reload extra dependency directive") { val sourceFilePath = os.rel / "ReloadTest.scala" val inputs = TestInputs( sourceFilePath -> s"""object ReloadTest { | println(os.pwd) |} |""".stripMargin ) inputs.fromRoot { root => os.proc(TestUtil.cli, "setup-ide", ".", extraOptions) .call( cwd = root, stdout = os.Inherit ) val ideOptionsPath = root / Constants.workspaceDirName / "ide-options-v2.json" val jsonOptions = List("--json-options", ideOptionsPath.toString) withBsp(inputs, Seq("."), bspOptions = jsonOptions, reuseRoot = Some(root)) { (_, _, remoteServer) => Future { val buildTargetsResp = remoteServer.workspaceBuildTargets().asScala.await val targets = buildTargetsResp.getTargets.asScala.map(_.getId).toSeq val resp = remoteServer.buildTargetCompile(new b.CompileParams(targets.asJava)) .asScala.await expect(resp.getStatusCode == b.StatusCode.ERROR) val depName = "os-lib" val depVersion = "0.8.1" val updatedSourceFile = s"""//> using dep com.lihaoyi::$depName:$depVersion | |object ReloadTest { | println(os.pwd) |} |""".stripMargin os.write.over(root / sourceFilePath, updatedSourceFile) val reloadResponse = extractWorkspaceReloadResponse(remoteServer.workspaceReload().asScala.await) expect(reloadResponse.isEmpty) val depSourcesParams = new b.DependencySourcesParams(targets.asJava) val depSourcesResponse = remoteServer.buildTargetDependencySources(depSourcesParams).asScala.await val depSources = depSourcesResponse.getItems.asScala.flatMap(_.getSources.asScala) expect(depSources.exists(s => s.contains(depName) && s.contains(depVersion))) } } } } test("workspace/reload of an extra sources directory") { val dir1 = "dir1" val dir2 = "dir2" val inputs = TestInputs( os.rel / dir1 / "ReloadTest.scala" -> s"""object ReloadTest { | val container = MissingCaseClass(value = "Hello") | println(container.value) |} |""".stripMargin ) val extraInputs = inputs.add( os.rel / dir2 / "MissingCaseClass.scala" -> "case class MissingCaseClass(value: String)" ) extraInputs.fromRoot { root => os.proc(TestUtil.cli, "setup-ide", dir1, extraOptions) .call( cwd = root, stdout = os.Inherit ) withBsp(inputs, Seq(dir1), reuseRoot = Some(root)) { (_, _, remoteServer) => Future { val buildTargetsResp = remoteServer.workspaceBuildTargets().asScala.await val targets = buildTargetsResp.getTargets.asScala.map(_.getId).toSeq val resp = remoteServer.buildTargetCompile(new b.CompileParams(targets.asJava)) .asScala.await expect(resp.getStatusCode == b.StatusCode.ERROR) os.proc(TestUtil.cli, "setup-ide", dir1, dir2, extraOptions) .call( cwd = root, stdout = os.Inherit ) val reloadResponse = extractWorkspaceReloadResponse(remoteServer.workspaceReload().asScala.await) expect(reloadResponse.isEmpty) val buildTargetsResp0 = remoteServer.workspaceBuildTargets().asScala.await val targets0 = buildTargetsResp0.getTargets.asScala.map(_.getId).toSeq val resp0 = remoteServer.buildTargetCompile(new b.CompileParams(targets0.asJava)) .asScala.await expect(resp0.getStatusCode == b.StatusCode.OK) } } } } test("workspace/reload error response when no inputs json present") { val inputs = TestInputs( os.rel / "ReloadTest.scala" -> s"""object ReloadTest { | println("Hello") |} |""".stripMargin ) withBsp(inputs, Seq(".")) { (_, _, remoteServer) => Future { val buildTargetsResp = remoteServer.workspaceBuildTargets().asScala.await val targets = buildTargetsResp.getTargets.asScala.map(_.getId).toSeq val resp = remoteServer.buildTargetCompile(new b.CompileParams(targets.asJava)) .asScala.await expect(resp.getStatusCode == b.StatusCode.OK) val reloadResp = remoteServer.workspaceReload().asScala.await val responseError = extractWorkspaceReloadResponse(reloadResp).getOrElse { sys.error(s"Unexpected workspace reload response shape $reloadResp") } expect(responseError.getCode == -32603) expect(responseError.getMessage.nonEmpty) } } } test("workspace/reload when updated source element in using directive") { val utilsFileName = "Utils.scala" val inputs = TestInputs( os.rel / "Hello.scala" -> s"""|//> using file Utils.scala | |object Hello extends App { | println("Hello World") |}""".stripMargin, os.rel / utilsFileName -> s"""|object Utils { | val hello = "Hello World" |}""".stripMargin ) withBsp(inputs, Seq("Hello.scala")) { (root, _, remoteServer) => Future { val buildTargetsResp = remoteServer.workspaceBuildTargets().asScala.await val target = { val targets = buildTargetsResp.getTargets.asScala.map(_.getId).toSeq expect(targets.length == 2) extractMainTargets(targets) } val targetUri = TestUtil.normalizeUri(target.getUri) checkTargetUri(root, targetUri) val targets = List(target).asJava val compileResp = remoteServer.buildTargetCompile(new b.CompileParams(targets)).asScala.await expect(compileResp.getStatusCode == b.StatusCode.OK) // after reload compilation should fail, Utils.scala file contains invalid scala code val updatedUtilsFile = s"""|object Utils { | val hello = "Hello World |}""".stripMargin os.write.over(root / utilsFileName, updatedUtilsFile) val buildTargetsResp0 = remoteServer.workspaceBuildTargets().asScala.await val targets0 = buildTargetsResp0.getTargets.asScala.map(_.getId).toSeq val resp0 = remoteServer.buildTargetCompile(new b.CompileParams(targets0.asJava)).asScala.await expect(resp0.getStatusCode == b.StatusCode.ERROR) } } } test("workspace/reload should restart bloop with correct JVM version from options") { val sourceFilePath = os.rel / "ReloadTest.java" val inputs = TestInputs( sourceFilePath -> s"""public class ReloadTest { | public static void main(String[] args) { | String a = "Hello World"; | | switch (a) { | case String s when s.length() > 6 -> System.out.println(s.toUpperCase()); | case String s -> System.out.println(s.toLowerCase()); | } | } |}""".stripMargin ) inputs.fromRoot { root => os.proc(TestUtil.cli, "--power", "bloop", "exit") .call( cwd = root, stdout = os.Inherit ) os.proc(TestUtil.cli, "setup-ide", ".", "--jvm", "11", extraOptions) .call( cwd = root, stdout = os.Inherit ) val ideOptionsPath = root / Constants.workspaceDirName / "ide-options-v2.json" val jsonOptions = List("--json-options", ideOptionsPath.toString) withBsp(inputs, Seq("."), bspOptions = jsonOptions, reuseRoot = Some(root)) { (_, _, remoteServer) => Future { val buildTargetsResp = remoteServer.workspaceBuildTargets().asScala.await val targets = buildTargetsResp.getTargets.asScala.map(_.getId).toSeq val errorResponse = remoteServer.buildTargetCompile(new b.CompileParams(targets.asJava)).asScala.await expect(errorResponse.getStatusCode == b.StatusCode.ERROR) val javacOptions = Seq( "--javac-opt", "--enable-preview", "--javac-opt", "--release", "--javac-opt", "19" ) os.proc(TestUtil.cli, "setup-ide", ".", "--jvm", "19", javacOptions, extraOptions) .call( cwd = root, stdout = os.Inherit ) val reloadResponse = extractWorkspaceReloadResponse(remoteServer.workspaceReload().asScala.await) expect(reloadResponse.isEmpty) val buildTargetsResp0 = remoteServer.workspaceBuildTargets().asScala.await val reloadedTargets = buildTargetsResp0.getTargets.asScala.map(_.getId).toSeq val okResponse = remoteServer.buildTargetCompile(new b.CompileParams(reloadedTargets.asJava)) .asScala.await expect(okResponse.getStatusCode == b.StatusCode.OK) } } } } test("workspace/reload should restart bloop with correct JVM version from directives") { val sourceFilePath = os.rel / "ReloadTest.java" val inputs = TestInputs( sourceFilePath -> s"""//> using jvm 11 | |public class ReloadTest { | public static void main(String[] args) { | System.out.println("Hello World"); | } |} |""".stripMargin ) inputs.fromRoot { root => os.proc(TestUtil.cli, "--power", "bloop", "exit") .call( cwd = root, stdout = os.Inherit ) os.proc(TestUtil.cli, "setup-ide", ".", extraOptions) .call( cwd = root, stdout = os.Inherit ) val ideOptionsPath = root / Constants.workspaceDirName / "ide-options-v2.json" val jsonOptions = List("--json-options", ideOptionsPath.toString) withBsp(inputs, Seq("."), bspOptions = jsonOptions, reuseRoot = Some(root)) { (_, localClient, remoteServer) => Future { val buildTargetsResp = remoteServer.workspaceBuildTargets().asScala.await val targets = buildTargetsResp.getTargets.asScala.map(_.getId).toSeq val resp = remoteServer.buildTargetCompile(new b.CompileParams(targets.asJava)) .asScala.await expect(resp.getStatusCode == b.StatusCode.OK) val updatedSourceFile = s"""//> using jvm 19 |//> using javacOpt --enable-preview --release 19 | |public class ReloadTest { | public static void main(String[] args) { | String a = "Hello World"; | | switch (a) { | case String s when s.length() > 6 -> System.out.println(s.toUpperCase()); | case String s -> System.out.println(s.toLowerCase()); | } | } |} |""".stripMargin os.write.over(root / sourceFilePath, updatedSourceFile) expect(!localClient.logMessages().exists(_.getMessage.startsWith( "Error reading API from class file: ReloadTest : java.lang.UnsupportedClassVersionError: ReloadTest has been compiled by a more recent version of the Java Runtime" ))) val errorResponse = remoteServer.buildTargetCompile(new b.CompileParams(targets.asJava)).asScala.await expect(errorResponse.getStatusCode == b.StatusCode.OK) expect(localClient.logMessages().exists(_.getMessage.startsWith( "Error reading API from class file: ReloadTest : java.lang.UnsupportedClassVersionError: ReloadTest has been compiled by a more recent version of the Java Runtime" ))) val reloadResponse = extractWorkspaceReloadResponse(remoteServer.workspaceReload().asScala.await) expect(reloadResponse.isEmpty) val buildTargetsResp0 = remoteServer.workspaceBuildTargets().asScala.await val reloadedTargets = buildTargetsResp0.getTargets.asScala.map(_.getId).toSeq val okResponse = remoteServer.buildTargetCompile(new b.CompileParams(reloadedTargets.asJava)) .asScala.await expect(okResponse.getStatusCode == b.StatusCode.OK) } } } } test("bsp should start bloop with correct JVM version from directives") { val sourceFilePath = os.rel / "ReloadTest.java" val inputs = TestInputs( sourceFilePath -> s"""//> using jvm 19 |//> using javacOpt --enable-preview --release 19 | |public class ReloadTest { | public static void main(String[] args) { | String a = "Hello World"; | | switch (a) { | case String s when s.length() > 6 -> System.out.println(s.toUpperCase()); | case String s -> System.out.println(s.toLowerCase()); | } | } |} |""".stripMargin ) inputs.fromRoot { root => os.proc(TestUtil.cli, "--power", "bloop", "exit") .call( cwd = root, stdout = os.Inherit ) os.proc(TestUtil.cli, "--power", "bloop", "start", "--jvm", "17") .call( cwd = root, stdout = os.Inherit ) os.proc(TestUtil.cli, "setup-ide", ".", extraOptions) .call( cwd = root, stdout = os.Inherit ) val ideOptionsPath = root / Constants.workspaceDirName / "ide-options-v2.json" val jsonOptions = List("--json-options", ideOptionsPath.toString) withBsp(inputs, Seq("."), bspOptions = jsonOptions, reuseRoot = Some(root)) { (_, _, remoteServer) => Future { val buildTargetsResp = remoteServer.workspaceBuildTargets().asScala.await val targets = buildTargetsResp.getTargets.asScala.map(_.getId).toSeq val resp = remoteServer.buildTargetCompile(new b.CompileParams(targets.asJava)).asScala.await expect(resp.getStatusCode == b.StatusCode.OK) } } } } test("bloop projects are initialised properly for an invalid directive value") { val inputs = TestInputs( os.rel / "InvalidUsingDirective.scala" -> s"""//> using scala true | |object InvalidUsingDirective extends App { | println("Hello") |} |""".stripMargin ) withBsp(inputs, Seq(".")) { (root, localClient, remoteServer) => Future { checkIfBloopProjectIsInitialised( root = root, buildTargetsResp = remoteServer.workspaceBuildTargets().asScala.await ) val diagnosticsParams = extractDiagnosticsParams(root / "InvalidUsingDirective.scala", localClient) val diagnostics = diagnosticsParams.getDiagnostics.asScala.toSeq expect(diagnostics.length == 1) checkDiagnostic( diagnostic = diagnostics.head, expectedMessage = """Encountered an error for the scala using directive. |Expected a string value, got 'true'""".stripMargin, expectedSeverity = b.DiagnosticSeverity.ERROR, expectedStartLine = 0, expectedStartCharacter = 16, expectedEndLine = 0, expectedEndCharacter = 20 ) } } } test("bloop projects are initialised properly for an unrecognised directive") { val sourceFileName = "UnrecognisedUsingDirective.scala" val directiveKey = "unrecognised.directive" val directiveValue = "value" val inputs = TestInputs( os.rel / sourceFileName -> s"""//> using $directiveKey $directiveValue | |object UnrecognisedUsingDirective extends App { | println("Hello") |} |""".stripMargin ) withBsp(inputs, Seq(".")) { (root, localClient, remoteServer) => Future { checkIfBloopProjectIsInitialised( root = root, buildTargetsResp = remoteServer.workspaceBuildTargets().asScala.await ) val diagnosticsParams = extractDiagnosticsParams(root / sourceFileName, localClient) val diagnostics = diagnosticsParams.getDiagnostics.asScala expect(diagnostics.length == 1) checkDiagnostic( diagnostic = diagnostics.head, expectedMessage = s"Unrecognized directive: $directiveKey with values: $directiveValue", expectedSeverity = b.DiagnosticSeverity.ERROR, expectedStartLine = 0, expectedStartCharacter = 10, expectedEndLine = 0, expectedEndCharacter = 32 ) } } } test("bloop projects are initialised properly for a directive for an unfetchable dependency") { val inputs = TestInputs( os.rel / "InvalidUsingDirective.scala" -> s"""//> using dep no::lib:123 | |object InvalidUsingDirective extends App { | println("Hello") |} |""".stripMargin ) withBsp(inputs, Seq(".")) { (root, localClient, remoteServer) => Future { checkIfBloopProjectIsInitialised( root = root, buildTargetsResp = remoteServer.workspaceBuildTargets().asScala.await ) val diagnosticsParams = extractDiagnosticsParams(root / "InvalidUsingDirective.scala", localClient) val diagnostics = diagnosticsParams.getDiagnostics.asScala.toSeq expect(diagnostics.length == 1) checkDiagnostic( diagnostic = diagnostics.head, expectedMessage = "Error downloading no:lib", expectedSeverity = b.DiagnosticSeverity.ERROR, expectedStartLine = 0, expectedStartCharacter = 14, expectedEndLine = 0, expectedEndCharacter = 25, strictlyCheckMessage = false ) } } } test("bsp should support parsing cancel params") { // TODO This test only checks if the native launcher of Scala CLI is able to parse cancel params, // this test does not check if Bloop supports $/cancelRequest. The status of that is tracked under the https://github.com/scalacenter/bloop/issues/2030. val fileName = "Hello.scala" val inputs = TestInputs( os.rel / fileName -> s"""object Hello extends App { | while(true) { | println("Hello World") | } |} |""".stripMargin ) withBsp(inputs, Seq("."), stdErrOpt = Some(os.rel / "stderr.txt")) { (root, _, remoteServer) => Future { // prepare build val buildTargetsResp = remoteServer.workspaceBuildTargets().asScala.await // build code val targets = buildTargetsResp.getTargets.asScala.map(_.getId()) val compileResp = remoteServer .buildTargetCompile(new b.CompileParams(targets.asJava)) .asScala .await expect(compileResp.getStatusCode == b.StatusCode.OK) val mainTarget = targets.find(!_.getUri.contains("-test")).get val runRespFuture = remoteServer .buildTargetRun(new b.RunParams(mainTarget)) runRespFuture.cancel(true) expect(runRespFuture.isCancelled || runRespFuture.isCompletedExceptionally) val stderrPath = root / "stderr.txt" expect(!os.read(stderrPath).contains("Unmatched cancel notification for request id null")) } } } test("bsp should report actionable diagnostic when enabled") { val fileName = "Hello.scala" val inputs = TestInputs( os.rel / fileName -> s"""//> using dep com.lihaoyi::os-lib:0.7.8 | |object Hello extends App { | println("Hello") |} |""".stripMargin ) withBsp(inputs, Seq(".", "--actions")) { (_, localClient, remoteServer) => Future { // prepare build val buildTargetsResp = remoteServer.workspaceBuildTargets().asScala.await // build code val targets = buildTargetsResp.getTargets.asScala.map(_.getId()).asJava remoteServer.buildTargetCompile(new b.CompileParams(targets)).asScala.await val visibleDiagnostics = localClient.diagnostics().takeWhile(!_.getReset).flatMap(_.getDiagnostics.asScala) expect(visibleDiagnostics.length == 1) val updateActionableDiagnostic = visibleDiagnostics.head checkDiagnostic( diagnostic = updateActionableDiagnostic, expectedMessage = "os-lib is outdated", expectedSeverity = b.DiagnosticSeverity.HINT, expectedStartLine = 0, expectedStartCharacter = 14, expectedEndLine = 0, expectedEndCharacter = 39, expectedSource = Some("scala-cli"), strictlyCheckMessage = false ) val scalaDiagnostic = new Gson().fromJson[b.ScalaDiagnostic]( updateActionableDiagnostic.getData.asInstanceOf[JsonElement], classOf[b.ScalaDiagnostic] ) val actions = scalaDiagnostic.getActions.asScala.toList assert(actions.size == 1) val changes = actions.head.getEdit.getChanges.asScala.toList assert(changes.size == 1) val textEdit = changes.head expect(textEdit.getNewText.contains("com.lihaoyi::os-lib:")) expect(textEdit.getRange.getStart.getLine == 0) expect(textEdit.getRange.getStart.getCharacter == 14) expect(textEdit.getRange.getEnd.getLine == 0) expect(textEdit.getRange.getEnd.getCharacter == 39) } } } if (actualScalaVersion.startsWith("3.")) List(".sc", ".scala").foreach { filetype => test(s"bsp should report actionable diagnostic from bloop for $filetype files (Scala 3)") { val fileName = s"Hello$filetype" val inputs = TestInputs( os.rel / fileName -> s""" |object Hello { | sealed trait TestTrait | case class TestA() extends TestTrait | case class TestB() extends TestTrait | val traitInstance: TestTrait = ??? | traitInstance match { | case TestA() => | } |} |""".stripMargin ) withBsp(inputs, Seq(".")) { (_, localClient, remoteServer) => Future { // prepare build val buildTargetsResp = remoteServer.workspaceBuildTargets().asScala.await // build code val targets = buildTargetsResp.getTargets.asScala.map(_.getId()).asJava remoteServer.buildTargetCompile(new b.CompileParams(targets)).asScala.await val visibleDiagnostics = localClient .diagnostics() .map(_.getDiagnostics.asScala) .find(_.nonEmpty) .getOrElse(Nil) expect(visibleDiagnostics.size == 1) val updateActionableDiagnostic = visibleDiagnostics.head checkDiagnostic( diagnostic = updateActionableDiagnostic, expectedMessage = "match may not be exhaustive.", expectedSeverity = b.DiagnosticSeverity.WARNING, expectedStartLine = 6, expectedStartCharacter = 2, expectedEndLine = 6, expectedEndCharacter = 15, expectedSource = Some("bloop"), strictlyCheckMessage = false ) val scalaDiagnostic = new Gson().fromJson[b.ScalaDiagnostic]( updateActionableDiagnostic.getData.asInstanceOf[JsonElement], classOf[b.ScalaDiagnostic] ) val actions = scalaDiagnostic.getActions.asScala.toList assert(actions.size == 1) val changes = actions.head.getEdit.getChanges.asScala.toList assert(changes.size == 1) val textEdit = changes.head expect(textEdit.getNewText.contains("\n case TestB() => ???")) expect(textEdit.getRange.getStart.getLine == 7) expect(textEdit.getRange.getStart.getCharacter == 19) expect(textEdit.getRange.getEnd.getLine == 7) expect(textEdit.getRange.getEnd.getCharacter == 19) } } } } test("bsp should support jvmRunEnvironment request") { val inputs = TestInputs( os.rel / "Hello.scala" -> s"""//> using dep com.lihaoyi::os-lib:0.7.8 | |object Hello extends App { | println("Hello") |} |""".stripMargin ) withBsp(inputs, Seq(".")) { (_, _, remoteServer) => Future { // prepare build val buildTargetsResp = remoteServer.workspaceBuildTargets().asScala.await // build code val targets = buildTargetsResp.getTargets.asScala.map(_.getId()).asJava val jvmRunEnvironmentResult: b.JvmRunEnvironmentResult = remoteServer .buildTargetJvmRunEnvironment(new b.JvmRunEnvironmentParams(targets)) .asScala .await expect(jvmRunEnvironmentResult.getItems.asScala.toList.nonEmpty) val jvmTestEnvironmentResult: b.JvmTestEnvironmentResult = remoteServer .buildTargetJvmTestEnvironment(new JvmTestEnvironmentParams(targets)) .asScala .await expect(jvmTestEnvironmentResult.getItems.asScala.toList.nonEmpty) } } } if (actualScalaVersion.startsWith("3")) test("@main in script") { val inputs = TestInputs( os.rel / "test.sc" -> """@main def main(args: Strings*): Unit = println("Args: " + args.mkString(" ")) |""".stripMargin ) withBsp(inputs, Seq(".")) { (root, localClient, remoteServer) => Future { val buildTargetsResp = remoteServer.workspaceBuildTargets().asScala.await val target = { val targets = buildTargetsResp.getTargets.asScala.map(_.getId).toSeq expect(targets.length == 2) extractMainTargets(targets) } val targetUri = TestUtil.normalizeUri(target.getUri) checkTargetUri(root, targetUri) val targets = List(target).asJava val compileResp = remoteServer .buildTargetCompile(new b.CompileParams(targets)) .asScala .await expect(compileResp.getStatusCode == b.StatusCode.ERROR) val diagnosticsParams = { val diagnostics = localClient.diagnostics() val params = diagnostics(2) expect(params.getBuildTarget.getUri == targetUri) val actualUri = TestUtil.normalizeUri(params.getTextDocument.getUri) val expectedUri = TestUtil.normalizeUri((root / "test.sc").toNIO.toUri.toASCIIString) expect(actualUri == expectedUri) params } val diagnostics = diagnosticsParams.getDiagnostics.asScala.toSeq expect(diagnostics.length == 1) checkDiagnostic( diagnostic = diagnostics.head, expectedMessage = "Annotation @main in .sc scripts is not supported, use .scala format instead", expectedSeverity = b.DiagnosticSeverity.ERROR, expectedStartLine = 0, expectedStartCharacter = 0, expectedEndLine = 0, expectedEndCharacter = 5 ) } } } def testSourceJars( directives: String = "//> using jar Message.jar", getBspOptions: os.RelPath => List[String] = _ => List.empty, checkTestTarget: Boolean = false ): Unit = { val jarSources = os.rel / "jarStuff" val mainSources = os.rel / "src" val jarPath = mainSources / "Message.jar" val sourceJarPath = mainSources / "Message-sources.jar" val inputs = TestInputs( jarSources / "Message.scala" -> "case class Message(value: String)", mainSources / "Main.scala" -> s"""$directives |object Main extends App { | println(Message("Hello").value) |} |""".stripMargin ) inputs.fromRoot { root => // package the library jar os.proc( TestUtil.cli, "--power", "package", jarSources, "--library", "-o", jarPath, extraOptions ) .call(cwd = root) // package the sources jar os.proc( TestUtil.cli, "--power", "package", jarSources, "--with-sources", "-o", sourceJarPath, extraOptions ) .call(cwd = root) withBsp( inputs, Seq(mainSources.toString), reuseRoot = Some(root), bspOptions = getBspOptions(sourceJarPath) ) { (_, _, remoteServer) => Future { val buildTargetsResp = remoteServer.workspaceBuildTargets().asScala.await val targets = buildTargetsResp .getTargets .asScala val mainTarget = targets.find(!_.getId.getUri.contains("-test")).get val testTarget = targets.find(_.getId.getUri.contains("-test")).get // ensure that the project compiles val compileRes = remoteServer.buildTargetCompile( new b.CompileParams(List(mainTarget.getId).asJava) ).asScala.await expect(compileRes.getStatusCode == b.StatusCode.OK) // ensure that the source jar is in the dependency sources val dependencySourcesResp = remoteServer .buildTargetDependencySources( new b.DependencySourcesParams(List(mainTarget.getId, testTarget.getId).asJava) ) .asScala .await val dependencySourceItems = dependencySourcesResp.getItems.asScala val sources = dependencySourceItems .filter(dsi => if (checkTestTarget) dsi.getTarget == testTarget.getId else dsi.getTarget == mainTarget.getId ) .flatMap(_.getSources.asScala) expect(sources.exists(_.endsWith(sourceJarPath.last))) } } } } test("source jars handled correctly from the command line") { testSourceJars(getBspOptions = sourceJarPath => List("--source-jar", sourceJarPath.toString)) } test( "source jars handled correctly from the command line smartly assuming a *-sources.jar is a source jar" ) { testSourceJars(getBspOptions = sourceJarPath => List("--extra-jar", sourceJarPath.toString)) } test("source jars handled correctly from a test scope using directive") { testSourceJars( directives = """//> using jar Message.jar |//> using test.sourceJar Message-sources.jar""".stripMargin, checkTestTarget = true ) } if (!actualScalaVersion.startsWith("2.12")) test("actionable diagnostics on deprecated using directives") { val inputs = TestInputs( os.rel / "test.sc" -> """//> using toolkit latest |//> using test.toolkit typelevel:latest | |//> using lib org.typelevel::cats-core:2.6.1 | |object Test extends App { | println("Hello") |} |""".stripMargin ) withBsp(inputs, Seq(".", "--actions=false")) { (root, localClient, remoteServer) => Future { val buildTargetsResp = remoteServer.workspaceBuildTargets().asScala.await val target = { val targets = buildTargetsResp.getTargets.asScala.map(_.getId).toSeq expect(targets.length == 2) extractMainTargets(targets) } val targetUri = TestUtil.normalizeUri(target.getUri) checkTargetUri(root, targetUri) val targets = List(target).asJava val compileResp = remoteServer .buildTargetCompile(new b.CompileParams(targets)) .asScala .await expect(compileResp.getStatusCode == b.StatusCode.OK) val diagnosticsParams = { val diagnostics = localClient.diagnostics() .filter(_.getReset == false) expect(diagnostics.size == 3) val params = diagnostics.head expect(params.getBuildTarget.getUri == targetUri) val actualUri = TestUtil.normalizeUri(params.getTextDocument.getUri) val expectedUri = TestUtil.normalizeUri((root / "test.sc").toNIO.toUri.toASCIIString) expect(actualUri == expectedUri) diagnostics } val diagnostics = diagnosticsParams.flatMap(_.getDiagnostics.asScala) .sortBy(_.getRange.getEnd.getCharacter()) { checkDiagnostic( diagnostic = diagnostics.head, expectedMessage = "Using 'latest' for toolkit is deprecated, use 'default' to get more stable behaviour", expectedSeverity = b.DiagnosticSeverity.WARNING, expectedStartLine = 0, expectedStartCharacter = 10, expectedEndLine = 0, expectedEndCharacter = 24 ) checkScalaAction( diagnostic = diagnostics.head, expectedActionsSize = 1, expectedTitle = "Change to: toolkit default", expectedChanges = 1, expectedStartLine = 0, expectedStartCharacter = 10, expectedEndLine = 0, expectedEndCharacter = 24, expectedNewText = "toolkit default" ) } { checkDiagnostic( diagnostic = diagnostics.apply(1), expectedMessage = "Using 'latest' for toolkit is deprecated, use 'default' to get more stable behaviour", expectedSeverity = b.DiagnosticSeverity.WARNING, expectedStartLine = 1, expectedStartCharacter = 10, expectedEndLine = 1, expectedEndCharacter = 39 ) checkScalaAction( diagnostic = diagnostics.apply(1), expectedActionsSize = 1, expectedTitle = "Change to: test.toolkit typelevel:default", expectedChanges = 1, expectedStartLine = 1, expectedStartCharacter = 10, expectedEndLine = 1, expectedEndCharacter = 39, expectedNewText = "test.toolkit typelevel:default" ) } { checkDiagnostic( diagnostic = diagnostics.apply(2), expectedMessage = "Using 'lib' is deprecated, use 'dep' instead", expectedSeverity = b.DiagnosticSeverity.WARNING, expectedStartLine = 3, expectedStartCharacter = 10, expectedEndLine = 3, expectedEndCharacter = 44 ) checkScalaAction( diagnostic = diagnostics.apply(2), expectedActionsSize = 1, expectedTitle = "Change to: dep org.typelevel::cats-core:2.6.1", expectedChanges = 1, expectedStartLine = 3, expectedStartCharacter = 10, expectedEndLine = 3, expectedEndCharacter = 44, expectedNewText = "dep org.typelevel::cats-core:2.6.1" ) } } } } test("BSP respects JAVA_HOME") { TestUtil.retryOnCi() { val javaVersion = "23" val inputs = TestInputs(os.rel / "check-java.sc" -> s"""assert(System.getProperty("java.version").startsWith("$javaVersion")) |println(System.getProperty("java.home"))""".stripMargin) inputs.fromRoot { root => os.proc(TestUtil.cli, "bloop", "exit", "--power").call(cwd = root) val java23Home = os.Path( os.proc(TestUtil.cs, "java-home", "--jvm", s"zulu:$javaVersion").call().out.trim(), os.pwd ) os.proc(TestUtil.cli, "setup-ide", "check-java.sc") .call(cwd = root, env = Map("JAVA_HOME" -> java23Home.toString())) val ideOptionsPath = root / Constants.workspaceDirName / "ide-options-v2.json" expect(ideOptionsPath.toNIO.toFile.exists()) val ideEnvsPath = root / Constants.workspaceDirName / "ide-envs.json" expect(ideEnvsPath.toNIO.toFile.exists()) val jsonOptions = List("--json-options", ideOptionsPath.toString) val envOptions = List("--envs-file", ideEnvsPath.toString) withBsp(inputs, Seq("."), bspOptions = jsonOptions ++ envOptions, reuseRoot = Some(root)) { (_, _, remoteServer) => Future { val targets = remoteServer.workspaceBuildTargets().asScala.await .getTargets.asScala .filter(!_.getId.getUri.contains("-test")) .map(_.getId()) val compileResult = remoteServer.buildTargetCompile(new b.CompileParams(targets.asJava)).asScala.await expect(compileResult.getStatusCode == b.StatusCode.OK) val runResult = remoteServer.buildTargetRun(new b.RunParams(targets.head)).asScala.await expect(runResult.getStatusCode == b.StatusCode.OK) } } } } } test("BSP respects --java-home") { TestUtil.retryOnCi() { val javaVersion = "23" val inputs = TestInputs(os.rel / "check-java.sc" -> s"""assert(System.getProperty("java.version").startsWith("$javaVersion")) |println(System.getProperty("java.home"))""".stripMargin) inputs.fromRoot { root => os.proc(TestUtil.cli, "bloop", "exit", "--power").call(cwd = root) val java22Home = os.Path( os.proc(TestUtil.cs, "java-home", "--jvm", s"zulu:$javaVersion").call().out.trim(), os.pwd ) os.proc(TestUtil.cli, "setup-ide", "check-java.sc", "--java-home", java22Home.toString()) .call(cwd = root) val ideOptionsPath = root / Constants.workspaceDirName / "ide-options-v2.json" expect(ideOptionsPath.toNIO.toFile.exists()) val jsonOptions = List("--json-options", ideOptionsPath.toString) withBsp(inputs, Seq("."), bspOptions = jsonOptions, reuseRoot = Some(root)) { (_, _, remoteServer) => Future { val targets = remoteServer.workspaceBuildTargets().asScala.await .getTargets.asScala .filter(!_.getId.getUri.contains("-test")) .map(_.getId()) val compileResult = remoteServer.buildTargetCompile(new b.CompileParams(targets.asJava)).asScala.await expect(compileResult.getStatusCode == b.StatusCode.OK) val runResult = remoteServer.buildTargetRun(new b.RunParams(targets.head)).asScala.await expect(runResult.getStatusCode == b.StatusCode.OK) } } } } } for { setPowerByLauncherOpt <- Seq(true, false) setPowerBySubCommandOpt <- Seq(true, false) setPowerByEnv <- Seq(true, false) setPowerByConfig <- Seq(true, false) powerIsSet = setPowerByLauncherOpt || setPowerBySubCommandOpt || setPowerByEnv || setPowerByConfig powerSettingDescription = { val launcherSetting = if (setPowerByLauncherOpt) "launcher option" else "" val subCommandSetting = if (setPowerBySubCommandOpt) "setup-ide option" else "" val envSetting = if (setPowerByEnv) "environment variable" else "" val configSetting = if (setPowerByConfig) "config" else "" List(launcherSetting, subCommandSetting, envSetting, configSetting) .filter(_.nonEmpty) .mkString(", ") } testDescription = if (powerIsSet) s"BSP respects --power mode set by $powerSettingDescription (example: using python directive)" else "BSP fails when --power mode is not set for experimental directives (example: using python directive)" } test(testDescription) { val scriptName = "requires-power.sc" val inputs = TestInputs(os.rel / scriptName -> s"""//> using python |println("scalapy is experimental")""".stripMargin) inputs.fromRoot { root => val configFile = os.rel / "config" / "config.json" val configEnvs = Map("SCALA_CLI_CONFIG" -> configFile.toString()) val setupIdeEnvs: Map[String, String] = if (setPowerByEnv) Map("SCALA_CLI_POWER" -> "true") ++ configEnvs else configEnvs val launcherOpts = if (setPowerByLauncherOpt) List("--power") else List.empty val subCommandOpts = if (setPowerBySubCommandOpt) List("--power") else List.empty val args = launcherOpts ++ List("setup-ide", scriptName) ++ subCommandOpts os.proc(TestUtil.cli, args).call(cwd = root, env = setupIdeEnvs) if (setPowerByConfig) os.proc(TestUtil.cli, "config", "power", "true") .call(cwd = root, env = configEnvs) val ideOptionsPath = root / Constants.workspaceDirName / "ide-options-v2.json" expect(ideOptionsPath.toNIO.toFile.exists()) val ideLauncherOptsPath = root / Constants.workspaceDirName / "ide-launcher-options.json" expect(ideLauncherOptsPath.toNIO.toFile.exists()) val ideEnvsPath = root / Constants.workspaceDirName / "ide-envs.json" expect(ideEnvsPath.toNIO.toFile.exists()) val jsonOptions = List( "--json-options", ideOptionsPath.toString, "--json-launcher-options", ideLauncherOptsPath.toString, "--envs-file", ideEnvsPath.toString ) withBsp( inputs, Seq("."), bspOptions = jsonOptions, bspEnvs = configEnvs, reuseRoot = Some(root) ) { (_, _, remoteServer) => Future { val targets = remoteServer.workspaceBuildTargets().asScala.await .getTargets.asScala .filter(!_.getId.getUri.contains("-test")) .map(_.getId()) val compileResult = remoteServer.buildTargetCompile(new b.CompileParams(targets.asJava)).asScala.await if powerIsSet then { expect(compileResult.getStatusCode == b.StatusCode.OK) val runResult = remoteServer.buildTargetRun(new b.RunParams(targets.head)).asScala.await expect(runResult.getStatusCode == b.StatusCode.OK) } else expect(compileResult.getStatusCode == b.StatusCode.ERROR) } } } } test("BSP reloads --power mode after setting it via env passed to setup-ide") { val scriptName = "requires-power.sc" val inputs = TestInputs(os.rel / scriptName -> s"""//> using python |println("scalapy is experimental")""".stripMargin) inputs.fromRoot { root => os.proc(TestUtil.cli, "setup-ide", scriptName, extraOptions).call(cwd = root) val ideEnvsPath = root / Constants.workspaceDirName / "ide-envs.json" expect(ideEnvsPath.toNIO.toFile.exists()) val jsonOptions = List("--envs-file", ideEnvsPath.toString) withBsp(inputs, Seq(scriptName), bspOptions = jsonOptions, reuseRoot = Some(root)) { (_, _, remoteServer) => Future { val targets = remoteServer.workspaceBuildTargets().asScala.await .getTargets.asScala .filter(!_.getId.getUri.contains("-test")) .map(_.getId()) // compilation should fail before reload, as --power mode is off val compileBeforeReloadResult = remoteServer.buildTargetCompile(new b.CompileParams(targets.asJava)).asScala.await expect(compileBeforeReloadResult.getStatusCode == b.StatusCode.ERROR) // enable --power mode via env for setup-ide os.proc(TestUtil.cli, "setup-ide", scriptName, extraOptions) .call(cwd = root, env = Map("SCALA_CLI_POWER" -> "true")) // compilation should now succeed val reloadResponse = extractWorkspaceReloadResponse(remoteServer.workspaceReload().asScala.await) expect(reloadResponse.isEmpty) val compileAfterReloadResult = remoteServer.buildTargetCompile(new b.CompileParams(targets.asJava)).asScala.await expect(compileAfterReloadResult.getStatusCode == b.StatusCode.OK) // code should also be runnable via BSP now val runResult = remoteServer.buildTargetRun(new b.RunParams(targets.head)).asScala.await expect(runResult.getStatusCode == b.StatusCode.OK) } } } } test("BSP reloads --power mode after setting it via config") { val scriptName = "requires-power.sc" val inputs = TestInputs(os.rel / scriptName -> s"""//> using python |println("scalapy is experimental")""".stripMargin) inputs.fromRoot { root => val configFile = os.rel / "config" / "config.json" val configEnvs = Map("SCALA_CLI_CONFIG" -> configFile.toString()) os.proc(TestUtil.cli, "setup-ide", scriptName, extraOptions).call( cwd = root, env = configEnvs ) val ideEnvsPath = root / Constants.workspaceDirName / "ide-envs.json" expect(ideEnvsPath.toNIO.toFile.exists()) val jsonOptions = List("--envs-file", ideEnvsPath.toString) withBsp( inputs, Seq(scriptName), bspOptions = jsonOptions, bspEnvs = configEnvs, reuseRoot = Some(root) ) { (_, _, remoteServer) => Future { val targets = remoteServer.workspaceBuildTargets().asScala.await .getTargets.asScala .filter(!_.getId.getUri.contains("-test")) .map(_.getId()) // compilation should fail before reload, as --power mode is off val compileBeforeReloadResult = remoteServer.buildTargetCompile(new b.CompileParams(targets.asJava)).asScala.await expect(compileBeforeReloadResult.getStatusCode == b.StatusCode.ERROR) // enable --power mode via config os.proc(TestUtil.cli, "config", "power", "true") .call(cwd = root, env = configEnvs) // compilation should now succeed val reloadResponse = extractWorkspaceReloadResponse(remoteServer.workspaceReload().asScala.await) expect(reloadResponse.isEmpty) val compileAfterReloadResult = remoteServer.buildTargetCompile(new b.CompileParams(targets.asJava)).asScala.await expect(compileAfterReloadResult.getStatusCode == b.StatusCode.OK) // code should also be runnable via BSP now val runResult = remoteServer.buildTargetRun(new b.RunParams(targets.head)).asScala.await expect(runResult.getStatusCode == b.StatusCode.OK) } } } } for { cliVersion <- Seq("1.8.4", "1.5.0", "1.0.0") } // TODO: test for nightly, too test(s"setup-ide doesn't pass unrecognised arguments to old --cli-versions: $cliVersion") { TestUtil.retryOnCi() { val scriptName = "cli-version.sc" val inputs = TestInputs( os.rel / scriptName -> s"""println("Hello from launcher v$cliVersion""" ) inputs.fromRoot { root => val r = os.proc( TestUtil.cli, "--cli-version", cliVersion, "setup-ide", scriptName, extraOptions ) .call(cwd = root, stderr = os.Pipe, check = false) expect(!r.err.text().contains("Unrecognized argument")) expect(r.exitCode == 0) } } } for { cliVersion <- Seq("1.8.4") } // TODO: test for nightly, too test( s"setup-ide prepares a valid BSP configuration with --cli-version $cliVersion" ) { TestUtil.retryOnCi() { val scriptName = "cli-version.sc" TestInputs(os.rel / scriptName -> s"""println("Hello from launcher v$cliVersion")""") .fromRoot { root => val cliVersionArgs = List("--cli-version", cliVersion) os.proc(TestUtil.cli, cliVersionArgs, "setup-ide", scriptName, extraOptions) .call(cwd = root) val expectedIdeLauncherFile = root / Constants.workspaceDirName / "ide-launcher-options.json" expect(expectedIdeLauncherFile.toNIO.toFile.exists()) expect(os.read(expectedIdeLauncherFile).contains(cliVersion)) val bspConfig = readBspConfig(root) expect(bspConfig.argv.head == TestUtil.cliPath) expect(bspConfig.argv.containsSlice(cliVersionArgs)) expect(bspConfig.argv.indexOfSlice(cliVersionArgs) < bspConfig.argv.indexOf("bsp")) } } } for { useScalaWrapper <- Seq(false, true) if actualScalaVersion.coursierVersion >= "3.5.0".coursierVersion scalaVersion = if actualScalaVersion == Constants.scala3NextRc then Constants.scala3NextRcAnnounced else if actualScalaVersion == Constants.scala3Next then Constants.scala3NextAnnounced else actualScalaVersion withLauncher = (root: os.Path) => (f: Seq[os.Shellable] => Unit) => if (useScalaWrapper) withScalaRunnerWrapper( root = root, localBin = root / "local-bin", localCache = Some(root / "local-cache"), scalaVersion = scalaVersion, shouldCleanUp = false )(launcher => f(Seq(launcher))) else f(Seq(TestUtil.cli)) launcherString = if (useScalaWrapper) s"scala wrapper ($scalaVersion)" else "Scala CLI" connectionJsonFileName = if (useScalaWrapper) "scala.json" else "scala-cli.json" } test( s"setup-ide prepares valid BSP connection json with a valid launcher ($launcherString)" ) { TestUtil.retryOnCi() { val scriptName = "example.sc" TestInputs( os.rel / scriptName -> s"""println("Hello")""" ) .fromRoot { root => withLauncher(root) { launcher => val jvmIndex = if (TestUtil.isJvmCli) Constants.minimumLauncherJavaVersion else 8 val javaHome = os.Path( os.proc(TestUtil.cs, "java-home", "--jvm", jvmIndex).call().out.trim(), os.pwd ) os.proc(launcher, "setup-ide", scriptName, extraOptions) .call(cwd = root, env = Map("JAVA_HOME" -> javaHome.toString)) val expectedIdeLauncherFile = root / Constants.workspaceDirName / "ide-launcher-options.json" expect(expectedIdeLauncherFile.toNIO.toFile.exists()) val bspConfig = readBspConfig(root, connectionJsonFileName) val bspLauncherCommand = { val launcherPrefix = bspConfig.argv.takeWhile(_ != TestUtil.cliPath) launcherPrefix :+ bspConfig.argv.drop(launcherPrefix.length).head } expect(bspLauncherCommand.last == TestUtil.cliPath) if (TestUtil.isJvmBootstrappedCli) { // this launcher is not self-executable and has to be launched with `java -jar` expect(bspLauncherCommand.head.endsWith("java")) expect(bspLauncherCommand.drop(1) == List("-jar", TestUtil.cliPath)) val bspJavaVersionResult = os.proc(bspLauncherCommand.head, "-version") .call( cwd = root, env = Map("JAVA_HOME" -> javaHome.toString), mergeErrIntoOut = true ) val bspJavaVersion = TestUtil.parseJavaVersion(bspJavaVersionResult.out.trim()).get // the bsp launcher has to know to run itself on a supported JVM expect(bspJavaVersion >= math.max( Constants.minimumInternalJvmVersion, Constants.bloopMinimumJvmVersion )) } else expect(bspLauncherCommand == List(TestUtil.cliPath)) val r = os.proc(bspLauncherCommand, "version", "--cli-version") .call(cwd = root, env = Map("JAVA_HOME" -> javaHome.toString)) expect(r.out.trim() == Constants.cliVersion) } } } } test("setup-ide passes Java props to the BSP configuration correctly") { val scriptName = "hello.sc" TestInputs(os.rel / scriptName -> s"""println("Hello")""").fromRoot { root => val javaProps = List("-Dfoo=bar", "-Dbar=baz") os.proc(TestUtil.cli, javaProps, "setup-ide", scriptName, extraOptions) .call(cwd = root) val bspConfig = readBspConfig(root) if (TestUtil.isJvmBootstrappedCli) { expect(bspConfig.argv.head.endsWith("java")) expect(bspConfig.argv.drop(1).head == "-jar") expect(bspConfig.argv.dropWhile(_ != TestUtil.cliPath).head == TestUtil.cliPath) } else expect(bspConfig.argv.head == TestUtil.cliPath) expect(bspConfig.argv.containsSlice(javaProps)) expect(bspConfig.argv.indexOfSlice(javaProps) < bspConfig.argv.indexOf("bsp")) } } test("BSP loads verbosity on compile") { val stderrFile = os.rel / "stderr.txt" val inputs = TestInputs( os.rel / "Hello.scala" -> s"""object Hello extends App { | println("Hello World") |} |""".stripMargin ) withBsp(inputs, Seq(".", "-v"), stdErrOpt = Some(stderrFile)) { (root, _, remoteServer) => Future { val buildTargetsResp = remoteServer.workspaceBuildTargets().asScala.await val targets = buildTargetsResp.getTargets.asScala.map(_.getId()) val compileResp = remoteServer .buildTargetCompile(new b.CompileParams(targets.asJava)) .asScala .await expect(compileResp.getStatusCode == b.StatusCode.OK) expect(os.read(root / stderrFile).contains("Scheduling compilation")) } } } test("BSP loads verbosity on compile when passed from setup-ide") { val stderrFile = os.rel / "stderr.txt" val inputs = TestInputs( os.rel / "Hello.scala" -> s"""object Hello extends App { | println("Hello World") |} |""".stripMargin ) inputs.fromRoot { root => os.proc(TestUtil.cli, "setup-ide", ".", "-v").call(cwd = root) val ideOptionsPath = root / Constants.workspaceDirName / "ide-options-v2.json" val jsonOptions = List("--json-options", ideOptionsPath.toString) withBsp( inputs = inputs, args = Seq("."), bspOptions = jsonOptions, reuseRoot = Some(root), stdErrOpt = Some(stderrFile) ) { (_, _, remoteServer) => Future { val buildTargetsResp = remoteServer.workspaceBuildTargets().asScala.await val targets = buildTargetsResp.getTargets.asScala.map(_.getId()) val compileResp = remoteServer .buildTargetCompile(new b.CompileParams(targets.asJava)) .asScala .await expect(compileResp.getStatusCode == b.StatusCode.OK) expect(os.read(root / stderrFile).contains("Scheduling compilation")) } } } } test("buildTarget/wrappedSources") { val inputs = TestInputs( os.rel / "simple.sc" -> s"""val msg = "Hello" |println(msg) |""".stripMargin ) withBsp(inputs, Seq(".")) { (root, _, remoteServer) => Future { val buildTargetsResp = remoteServer.workspaceBuildTargets().asScala.await val target = { val targets = buildTargetsResp.getTargets.asScala.map(_.getId).toSeq expect(targets.length == 2) extractMainTargets(targets) } val targets = List(target).asJava val compileResp = remoteServer.buildTargetCompile(new b.CompileParams(targets)).asScala.await expect(compileResp.getStatusCode == b.StatusCode.OK) val resp = remoteServer .buildTargetWrappedSources(new b.SourcesParams(targets)) .asScala .await val items = resp.items.asScala expect(items.nonEmpty) val mainItem = items.find(_.target.getUri == target.getUri).get val sources = mainItem.sources.asScala expect(sources.size == 1) val wrappedSource = sources.head val sourceUri = TestUtil.normalizeUri(wrappedSource.uri) val expectedUri = TestUtil.normalizeUri((root / "simple.sc").toNIO.toUri.toASCIIString) expect(sourceUri == expectedUri) expect(wrappedSource.topWrapper.contains("simple")) expect(wrappedSource.topWrapper.contains("scriptPath")) expect(wrappedSource.bottomWrapper == "}") expect(wrappedSource.generatedUri.endsWith(".scala")) expect(wrappedSource.generatedUri.contains("simple")) } } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/BspTests212.scala ================================================ package scala.cli.integration class BspTests212 extends BspTestDefinitions with BspTests2Definitions with Test212 ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/BspTests213.scala ================================================ package scala.cli.integration import ch.epfl.scala.bsp4j as b import com.eed3si9n.expecty.Expecty.expect import com.google.gson.{Gson, JsonElement} import scala.cli.integration.TestUtil.await import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future import scala.jdk.CollectionConverters.* class BspTests213 extends BspTestDefinitions with BspTests2Definitions with Test213 { List(".sc", ".scala").foreach { filetype => test(s"bsp should report actionable diagnostic from bloop for $filetype files (Scala 2.13)") { val fileName = s"Hello$filetype" val inputs = TestInputs( os.rel / fileName -> s""" |object Hello { | def foo: Any = { | x: Int => x * 2 | } |} |""".stripMargin ) withBsp(inputs, Seq(".", "-O", "-Xsource:3")) { (_, localClient, remoteServer) => Future { // prepare build val buildTargetsResp = remoteServer.workspaceBuildTargets().asScala.await // build code val targets = buildTargetsResp.getTargets.asScala.map(_.getId()).asJava remoteServer.buildTargetCompile(new b.CompileParams(targets)).asScala.await val visibleDiagnostics = localClient .diagnostics() .map(_.getDiagnostics.asScala) .find(_.nonEmpty) .getOrElse(Nil) expect(visibleDiagnostics.size == 1) val updateActionableDiagnostic = visibleDiagnostics.head checkDiagnostic( diagnostic = updateActionableDiagnostic, expectedMessage = "parentheses are required around the parameter of a lambda", expectedSeverity = b.DiagnosticSeverity.ERROR, expectedStartLine = 3, expectedStartCharacter = 5, expectedEndLine = 3, expectedEndCharacter = 5, expectedSource = Some("bloop"), strictlyCheckMessage = false ) val scalaDiagnostic = new Gson().fromJson[b.ScalaDiagnostic]( updateActionableDiagnostic.getData.asInstanceOf[JsonElement], classOf[b.ScalaDiagnostic] ) val actions = scalaDiagnostic.getActions.asScala.toList assert(actions.size == 1) val changes = actions.head.getEdit.getChanges.asScala.toList assert(changes.size == 1) val textEdit = changes.head expect(textEdit.getNewText.contains("(x: Int)")) expect(textEdit.getRange.getStart.getLine == 3) expect(textEdit.getRange.getStart.getCharacter == 4) expect(textEdit.getRange.getEnd.getLine == 3) expect(textEdit.getRange.getEnd.getCharacter == 10) } } } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/BspTests2Definitions.scala ================================================ package scala.cli.integration import scala.concurrent.ExecutionContext.Implicits.global trait BspTests2Definitions { this: BspTestDefinitions => for { useDirectives <- Seq(true, false) (directive, options) <- Seq( (s"//> using scala $actualScalaVersion", Seq("--scala", actualScalaVersion)) ) extraOptionsOverride = if (useDirectives) TestUtil.extraOptions else TestUtil.extraOptions ++ options testNameSuffix = if (useDirectives) directive else options.mkString(" ") } test(s"BSP App object wrapper forced with $testNameSuffix") { val (script1, script2) = "script1.sc" -> "script2.sc" val directiveString = if (useDirectives) directive else "" val inputs = TestInputs( os.rel / script1 -> s"""//> using platform js |$directiveString | |def main(args: String*): Unit = println("Hello") |main() |""".stripMargin, os.rel / script2 -> """println("Hello") |""".stripMargin ) testScriptWrappers(inputs, extraOptionsOverride = extraOptionsOverride)(expectAppWrapper) } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/BspTests3Definitions.scala ================================================ package scala.cli.integration import scala.concurrent.ExecutionContext.Implicits.global trait BspTests3Definitions { this: BspTestDefinitions => test("BSP class wrapper for Scala 3") { val (script1, script2) = "script1.sc" -> "script2.sc" val inputs = TestInputs( os.rel / script1 -> s"""def main(args: String*): Unit = println("Hello") |main() |""".stripMargin, os.rel / script2 -> s"""//> using dep org.scalatest::scalatest:3.2.15 | |import org.scalatest.*, flatspec.*, matchers.* | |class PiTest extends AnyFlatSpec with should.Matchers { | "pi calculus" should "return a precise enough pi value" in { | math.Pi shouldBe 3.14158d +- 0.001d | } |} |org.scalatest.tools.Runner.main(Array("-oDF", "-s", classOf[PiTest].getName))""".stripMargin ) testScriptWrappers(inputs)(expectClassWrapper) } for { useDirectives <- Seq(true, false) (directive, options) <- Seq( ("//> using object.wrapper", Seq("--object-wrapper")), ("//> using platform js", Seq("--js")) ) wrapperOptions = if (useDirectives) Nil else options testNameSuffix = if (useDirectives) directive else options.mkString(" ") } test(s"BSP object wrapper forced with $testNameSuffix") { val (script1, script2) = "script1.sc" -> "script2.sc" val directiveString = if (useDirectives) directive else "" val inputs = TestInputs( os.rel / script1 -> s"""$directiveString | |def main(args: String*): Unit = println("Hello") |main() |""".stripMargin, os.rel / script2 -> """println("Hello") |""".stripMargin ) testScriptWrappers(inputs, bspOptions = wrapperOptions)(expectObjectWrapper) } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/BspTests3Lts.scala ================================================ package scala.cli.integration class BspTests3Lts extends BspTestDefinitions with BspTests3Definitions with Test3Lts ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/BspTests3NextRc.scala ================================================ package scala.cli.integration import ch.epfl.scala.bsp4j as b import com.eed3si9n.expecty.Expecty.expect import scala.cli.integration.TestUtil.await import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future import scala.jdk.CollectionConverters.* import scala.util.Properties class BspTests3NextRc extends BspTestDefinitions with BspTests3Definitions with Test3NextRc { test("BSP respects --cli-default-scala-version & --predefined-repository launcher options") { // 3.5.0-RC1-fakeversion-bin-SNAPSHOT has too long filenames for Windows. // Yes, seriously. Which is why we can't use it there. val sv = if (Properties.isWin) Constants.scala3NextRc else "3.5.0-RC1-fakeversion-bin-SNAPSHOT" val inputs = TestInputs( os.rel / "simple.sc" -> s"""assert(dotty.tools.dotc.config.Properties.versionNumberString == "$sv")""" ) inputs.fromRoot { root => os.proc(TestUtil.cli, "bloop", "exit", "--power").call(cwd = root) val localRepoPath = root / "local-repo" if (Properties.isWin) { val artifactNames = Seq( "scala3-compiler_3", "scala3-staging_3", "scala3-tasty-inspector_3", "scala3-sbt-bridge" ) for { artifactName <- artifactNames } { val csRes = os.proc( TestUtil.cs, "fetch", "--cache", localRepoPath, s"org.scala-lang:$artifactName:$sv" ) .call(cwd = root) expect(csRes.exitCode == 0) } } else { TestUtil.initializeGit(root) os.proc( "git", "clone", "https://github.com/dotty-staging/maven-test-repo.git", localRepoPath.toString ).call(cwd = root) } val predefinedRepository = if (Properties.isWin) (localRepoPath / "https" / "repo1.maven.org" / "maven2").toNIO.toUri.toASCIIString else (localRepoPath / "thecache" / "https" / "repo1.maven.org" / "maven2").toNIO.toUri.toASCIIString os.proc( TestUtil.cli, "--cli-default-scala-version", sv, "--predefined-repository", predefinedRepository, "setup-ide", "simple.sc", "--with-compiler", "--offline", "--power" ) .call(cwd = root) val ideOptionsPath = root / Constants.workspaceDirName / "ide-options-v2.json" expect(ideOptionsPath.toNIO.toFile.exists()) val ideEnvsPath = root / Constants.workspaceDirName / "ide-envs.json" expect(ideEnvsPath.toNIO.toFile.exists()) val ideLauncherOptionsPath = root / Constants.workspaceDirName / "ide-launcher-options.json" expect(ideLauncherOptionsPath.toNIO.toFile.exists()) val jsonOptions = List("--json-options", ideOptionsPath.toString) val launcherOptions = List("--json-launcher-options", ideLauncherOptionsPath.toString) val envOptions = List("--envs-file", ideEnvsPath.toString) val bspOptions = jsonOptions ++ launcherOptions ++ envOptions withBsp(inputs, Seq("."), bspOptions = bspOptions, reuseRoot = Some(root)) { (_, _, remoteServer) => Future { val targets = remoteServer.workspaceBuildTargets().asScala.await .getTargets.asScala .filter(!_.getId.getUri.contains("-test")) .map(_.getId()) val compileResult = remoteServer.buildTargetCompile(new b.CompileParams(targets.asJava)).asScala.await expect(compileResult.getStatusCode == b.StatusCode.OK) val runResult = remoteServer.buildTargetRun(new b.RunParams(targets.head)).asScala.await expect(runResult.getStatusCode == b.StatusCode.OK) } } } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/BspTestsDefault.scala ================================================ package scala.cli.integration class BspTestsDefault extends BspTestDefinitions with BspTests3Definitions with TestDefault ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/CleanTests.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect class CleanTests extends ScalaCliSuite { override def group: ScalaCliSuite.TestGroup = ScalaCliSuite.TestGroup.First test("simple") { val inputs = TestInputs( os.rel / "Hello.scala" -> """object Hello { | def main(args: Array[String]): Unit = | println("Hello") |} |""".stripMargin ) inputs.fromRoot { root => val dir = root / Constants.workspaceDirName val bspEntry = root / ".bsp" / "scala-cli.json" val res = os.proc(TestUtil.cli, "run", ".").call(cwd = root) expect(res.out.trim() == "Hello") expect(os.exists(dir)) expect(os.exists(bspEntry)) os.proc(TestUtil.cli, "clean", ".").call(cwd = root) expect(!os.exists(dir)) expect(!os.exists(bspEntry)) } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/CompileScalacCompatTestDefinitions.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect import scala.util.Properties /** For the `run` counterpart, refer to [[RunScalacCompatTestDefinitions]] */ trait CompileScalacCompatTestDefinitions { this: CompileTestDefinitions => if (actualScalaVersion.startsWith("3")) test("consecutive -language:* flags are not ignored") { val sourceFileName = "example.scala" TestInputs(os.rel / sourceFileName -> s"""//> using scala $actualScalaVersion |//> using options -color:never -language:noAutoTupling -language:strictEquality |case class Cat(name: String) |case class Dog(name: String) |def strictEquality(c: Cat, d: Dog):Boolean = c == d |def takesTuple(tpl: Tuple) = ??? |def withTuple() = takesTuple(1, 2) |""".stripMargin).fromRoot { root => val res = os.proc(TestUtil.cli, "compile", sourceFileName) .call(cwd = root, check = false, stderr = os.Pipe) expect(res.exitCode == 1) val errOutput = res.err.trim() val expectedStrictEqualityError = " Values of types Cat and Dog cannot be compared with == or !=" expect(errOutput.contains(expectedStrictEqualityError)) val expectedNoAutoTuplingError = "too many arguments for method takesTuple: (tpl: Tuple): Nothing" expect(errOutput.trim().contains(expectedNoAutoTuplingError)) } } // Given the vast number of ways compiler options can be passed from the CLI, // we test them all (or most, at the very least), as a (perhaps overkill) sanity check. // Pieces of the existing `-language:*` test are reused, but kept separate for clarity. { val modes @ Seq(viaDirective, viaCli, viaCliWithExplicitOpt, mixed, mixedWithExplicitOpt) = Seq("directive", "cli", "cli with -O", "mixed", "mixed with -O") for { mode <- modes if actualScalaVersion == Constants.scala3Next dashPrefix <- Seq("-", "--") syntaxVariant <- Seq( Seq( Seq(s"${dashPrefix}color:never"), Seq(s"${dashPrefix}language:noAutoTupling"), Seq(s"${dashPrefix}language:strictEquality") ), Seq( Seq(s"${dashPrefix}color", "never"), Seq(s"${dashPrefix}language", "noAutoTupling"), Seq(s"${dashPrefix}language", "strictEquality") ), Seq( Seq(s"${dashPrefix}color:never"), Seq(s"${dashPrefix}language:noAutoTupling,strictEquality") ), Seq( Seq(s"${dashPrefix}color", "never"), Seq(s"${dashPrefix}language", "noAutoTupling,strictEquality") ) ) (cliOpts, directiveOpts) = { val (initialCliOpts, initialDirectiveOpts) = mode match { case m if m == mixed => syntaxVariant.splitAt(syntaxVariant.length - 1) case m if m == mixedWithExplicitOpt => val (initialCliOpts, initialDirectiveOpts) = syntaxVariant.splitAt(syntaxVariant.length - 1) initialCliOpts.map(_.flatMap(o => Seq("-O", o))) -> initialDirectiveOpts case c if c == viaCli => syntaxVariant -> Nil case c if c == viaCliWithExplicitOpt => syntaxVariant.map(_.flatMap(o => Seq("-O", o))) -> Nil case _ => Nil -> syntaxVariant } initialCliOpts.flatten.map(_.filter(_ != '"')) -> initialDirectiveOpts.flatten } cliOptsString = cliOpts.mkString(" ") directiveOptsString = directiveOpts.mkString(" ") includeDirective = (mode == viaDirective || mode == mixed || mode == mixedWithExplicitOpt) && directiveOpts.nonEmpty directiveString = if (includeDirective) s"//> using options $directiveOptsString" else "" allOptsString = mode match { case m if m.startsWith(mixed) => s"opts passed via command line: $cliOptsString, opts passed via directive: $directiveString" case c if c.startsWith(viaCli) => s"opts passed via command line: $cliOptsString" case _ => s"opts passed via directive: $directiveString" } } test(s"compiler options passed in $mode mode: $allOptsString") { val sourceFileName = "example.scala" TestInputs(os.rel / sourceFileName -> s"""//> using scala $actualScalaVersion |$directiveString |case class Cat(name: String) |case class Dog(name: String) |def strictEquality(c: Cat, d: Dog):Boolean = c == d |def takesTuple(tpl: Tuple) = ??? |def withTuple() = takesTuple(1, 2) |""".stripMargin).fromRoot { root => val res = os.proc(TestUtil.cli, "compile", sourceFileName, cliOpts) .call(cwd = root, check = false, stderr = os.Pipe) println(res.err.trim()) expect(res.exitCode == 1) val errOutput = res.err.trim() val expectedStrictEqualityError = "Values of types Cat and Dog cannot be compared with == or !=" expect(errOutput.contains(expectedStrictEqualityError)) val expectedNoAutoTuplingError = "too many arguments for method takesTuple: (tpl: Tuple): Nothing" expect(errOutput.trim().contains(expectedNoAutoTuplingError)) } } } for { useDirective <- Seq(true, false) if !Properties.isWin optionsSource = if (useDirective) "using directive" else "command line" sv = actualScalaVersion } { test(s"consecutive -Wconf:* flags are not ignored (passed via $optionsSource)") { val sourceFileName = "example.scala" val warningConfOptions = Seq("-Wconf:cat=deprecation:e", "-Wconf:any:s") val maybeDirectiveString = if (useDirective) s"//> using options ${warningConfOptions.mkString(" ")}" else "" TestInputs(os.rel / sourceFileName -> s"""//> using scala $sv |$maybeDirectiveString |object WConfExample extends App { | @deprecated("This method will be removed", "1.0.0") | def oldMethod(): Unit = println("This is an old method.") | oldMethod() |} |""".stripMargin).fromRoot { root => val localBin = root / "local-bin" os.proc( TestUtil.cs, "install", "--install-dir", localBin, s"scalac:$sv" ).call(cwd = root) val cliRes = os.proc( TestUtil.cli, "compile", sourceFileName, "--server=false", if (useDirective) Nil else warningConfOptions ) .call(cwd = root, check = false, stderr = os.Pipe) val scalacRes = os.proc(localBin / "scalac", warningConfOptions, sourceFileName) .call(cwd = root, check = false, stderr = os.Pipe) expect(scalacRes.exitCode == cliRes.exitCode) val scalacResErr = scalacRes.err.trim() val cliResErr = cliRes.err.trim().linesIterator.toList // skip potentially irrelevant logs .dropWhile(_.contains("Check")) .mkString(System.lineSeparator()) expect(cliResErr == scalacResErr) } } if (!sv.startsWith("2.12")) test(s"consecutive -Wunused:* flags are not ignored (passed via $optionsSource)") { val sourceFileName = "example.scala" val unusedLintOptions = Seq("-Wunused:locals", "-Wunused:privates") val maybeDirectiveString = if (useDirective) s"//> using options ${unusedLintOptions.mkString(" ")}" else "" TestInputs(os.rel / sourceFileName -> s"""//> using scala $sv |$maybeDirectiveString |object WUnusedExample { | private def unusedPrivate(): String = "stuff" | def methodWithUnusedLocal() = { | val smth = "hello" | println("Hello") | } |} |""".stripMargin).fromRoot { root => val r = os.proc( TestUtil.cli, "compile", sourceFileName, if (useDirective) Nil else unusedLintOptions ) .call(cwd = root, stderr = os.Pipe) val err = r.err.trim() val unusedKeyword = if (sv.startsWith("2")) "never used" else "unused" expect(err.linesIterator.exists(l => l.contains(unusedKeyword) && l.contains("local"))) expect(err.linesIterator.exists(l => l.contains(unusedKeyword) && l.contains("private"))) } } } { val prefixes = Seq("-", "--") for { prefix1 <- prefixes prefix2 <- prefixes optionKey = "Werror" option1 = prefix1 + optionKey option2 = prefix2 + optionKey if actualScalaVersion.startsWith("3") } test( s"allow to override $option1 compiler option passed via directive by passing $option2 from the command line" ) { val file = "example.scala" TestInputs(os.rel / file -> s"""//> using options -Wunused:all $option1 |@main def main() = { | val unused = "" | println("Hello, world!") |} |""".stripMargin).fromRoot { root => os.proc( TestUtil.cli, "compile", file, s"$option2:false", extraOptions ) .call(cwd = root, stderr = os.Pipe) } } } for { scalaVersion <- Seq("3.nightly", "3.8.0-RC1-bin-20250825-ee2f641-NIGHTLY") withBloop <- Seq(false, true) withBloopString = if (withBloop) "with Bloop" else "scalac" buildServerOpts = if (withBloop) Nil else Seq("--server=false") if (!Properties.isWin || withBloop) && actualScalaVersion == Constants.scala3Next } test(s"sanity check for Scala $scalaVersion standard library with cc ($withBloopString)") { TestUtil.retryOnCi() { val input = "example.scala" TestInputs(os.rel / input -> s"""//> using scala $scalaVersion |import language.experimental.captureChecking | |trait File extends caps.SharedCapability: | def count(): Int | |def f(file: File): IterableOnce[Int]^{file} = | Iterator(1) | .map(_ + file.count()) |""".stripMargin).fromRoot { root => os.proc(TestUtil.cli, "compile", input, buildServerOpts).call(cwd = root) } } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/CompileTestDefinitions.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect import java.io.File import scala.cli.integration.TestUtil.ProcOps import scala.cli.integration.util.BloopUtil import scala.concurrent.duration.DurationInt import scala.util.Properties abstract class CompileTestDefinitions extends ScalaCliSuite with TestScalaVersionArgs with CompilerPluginTestDefinitions with CompileScalacCompatTestDefinitions with SemanticDbTestDefinitions { this: TestScalaVersion => protected lazy val extraOptions: Seq[String] = scalaVersionArgs ++ TestUtil.extraOptions private lazy val bloopDaemonDir = BloopUtil.bloopDaemonDir { os.proc(TestUtil.cli, "--power", "directories").call().out.text() } val simpleInputs: TestInputs = TestInputs( os.rel / "MyTests.scala" -> """//> using dep com.lihaoyi::os-lib::0.8.1 | |object MyTests { | def main(args: Array[String]): Unit = { | for (l <- os.list(os.pwd)) | println(l.last) | } |} |""".stripMargin ) val mainAndTestInputs: TestInputs = TestInputs( os.rel / "Main.scala" -> """//> using dep com.lihaoyi::utest:0.7.10 | |object Main { | val err = utest.compileError("pprint.log(2)") | def message = "Hello from " + "tests" | def main(args: Array[String]): Unit = { | println(message) | println(err) | } |} |""".stripMargin, os.rel / "Tests.test.scala" -> """//> using dep com.lihaoyi::pprint:0.6.6 | |import utest._ | |object Tests extends TestSuite { | val tests = Tests { | test("message") { | assert(Main.message.startsWith("Hello")) | } | } |} |""".stripMargin ) { val inputs = TestInputs( os.rel / "Bar.java" -> """public class Bar {} |""".stripMargin, os.rel / "Foo.java" -> """public class Foo {} |""".stripMargin ) test( "java files with no using directives should not produce warnings about using directives in multiple files" ) { inputs.fromRoot { root => val warningMessage = "Using directives detected in multiple files" val output = os.proc(TestUtil.cli, "compile", extraOptions, ".") .call(cwd = root, stderr = os.Pipe).err.trim() expect(!output.contains(warningMessage)) } } test("Pure Java with --server=false: no warning about .java files not being compiled") { inputs.fromRoot { root => val warningMessage = ".java files are not compiled to .class files" val output = os.proc(TestUtil.cli, "compile", "--server=false", extraOptions, ".") .call(cwd = root, stderr = os.Pipe).err.text() expect(!output.contains(warningMessage)) } } } test("with one file per scope, no warning about spread directives should be printed") { TestInputs( os.rel / "Bar.scala" -> """//> using dep com.lihaoyi::os-lib:0.9.1 | |object Bar extends App { | println(os.pwd) |} |""".stripMargin, os.rel / "Foo.test.scala" -> """//> using dep org.scalameta::munit:0.7.29 | |class Foo extends munit.FunSuite { | test("Hello") { | assert(true) | } |} |""".stripMargin ).fromRoot { root => val warningMessage = "Using directives detected in multiple files" val output = os.proc(TestUtil.cli, "compile", ".", "--test", extraOptions) .call(cwd = root, stderr = os.Pipe).err.trim() expect(!output.contains(warningMessage)) } } test("with >1 file per scope, the warning about spread directives should be printed") { TestInputs( os.rel / "Bar.scala" -> """//> using dep com.lihaoyi::os-lib:0.9.1 | |object Bar extends App { | pprint.pprintln(Foo(os.pwd.toString).value) |} |""".stripMargin, os.rel / "Foo.scala" -> """//> using dep com.lihaoyi::pprint:0.9.6 | |case class Foo(value: String) |""".stripMargin, os.rel / "Foo.test.scala" -> """//> using dep org.scalameta::munit:0.7.29 | |class FooTest extends munit.FunSuite { | test("Hello") { | assert(true) | } |} |""".stripMargin ).fromRoot { root => val warningMessage = "Using directives detected in multiple files" val output = os.proc(TestUtil.cli, "compile", ".", "--test", extraOptions) .call(cwd = root, stderr = os.Pipe).err.trim() expect(output.contains(warningMessage)) } } test( "having target + using directives in files: no using-directives or .java-not-compiled warnings" ) { val inputs = TestInputs( os.rel / "Bar.java" -> """//> using target.platform jvm |//> using jvm 17 |public class Bar {} |""".stripMargin, os.rel / "Foo.test.scala" -> """//> using target.scala.>= 2.13 |//> using dep com.lihaoyi::os-lib::0.8.1 |class Foo {} |""".stripMargin ) inputs.fromRoot { root => val warningMessage = "Using directives detected in multiple files" val output = os.proc(TestUtil.cli, "--power", "compile", extraOptions, ".") .call(cwd = root).err.trim() expect(!output.contains(warningMessage)) expect(!output.contains(".java files are not compiled to .class files")) } } { val javaSourceFile = "Bar.java" val inputs = TestInputs( os.rel / javaSourceFile -> """//> using jvm 17 |public class Bar {} |""".stripMargin, os.rel / "Foo.scala" -> """//> using dep com.lihaoyi::os-lib::0.8.1 |class Foo {} |""".stripMargin ) test("warn about directives in multiple files") { inputs.fromRoot { root => val warningMessage = "Using directives detected in multiple files" val output = os.proc(TestUtil.cli, "--power", "compile", extraOptions, ".") .call(cwd = root, stderr = os.Pipe).err.trim() expect(output.contains(warningMessage)) } } test("mixed .java/.scala: with --server=false warn about .java not compiled") { inputs.fromRoot { root => val warningMessage = ".java files are not compiled to .class files" val output = os.proc(TestUtil.cli, "--power", "compile", extraOptions, ".", "--server=false") .call(cwd = root, stderr = os.Pipe).err.trim() expect(output.contains(warningMessage)) expect(output.contains(javaSourceFile)) } } } test("no arg") { simpleInputs.fromRoot { root => os.proc(TestUtil.cli, "compile", extraOptions, ".").call(cwd = root) } } test("exit code") { val inputs = TestInputs( os.rel / "Main.scala" -> """object Main { | def main(args: Array[String]): Unit = | println(nope) |} |""".stripMargin ) inputs.fromRoot { root => val res = os.proc(TestUtil.cli, "compile", extraOptions, ".") .call(cwd = root, check = false, stderr = os.Pipe, mergeErrIntoOut = true) expect(res.exitCode == 1) } } def checkIfCompileOutputIsCopied(baseName: String, output: os.Path): Unit = { val extraExtensions = if (actualScalaVersion.startsWith("2.")) Nil else Seq(".tasty") val extensions = Seq(".class", "$.class") ++ extraExtensions val foundFiles = os.list(output).map(_.relativeTo(output)) val expectedFiles = extensions.map(ext => os.rel / s"$baseName$ext") expect(foundFiles.toSet == expectedFiles.toSet) } test("copy compile output") { mainAndTestInputs.fromRoot { root => val tempOutput = root / "output" os.proc(TestUtil.cli, "compile", "--compile-output", tempOutput, extraOptions, ".").call(cwd = root ) checkIfCompileOutputIsCopied("Main", tempOutput) } } test("test scope") { mainAndTestInputs.fromRoot { root => val tempOutput = root / "output" val output = os.proc( TestUtil.cli, "compile", "--test", "--compile-output", tempOutput, "--print-class-path", extraOptions, "." ).call(cwd = root ).out.trim() val classPath = output.split(File.pathSeparator).map(_.trim).filter(_.nonEmpty) val isDefinedTestPathInClassPath = // expected test class path - root / Constants.workspaceDirName / project_(hash) / classes / test classPath.exists(p => p.startsWith((root / Constants.workspaceDirName).toString()) && p.endsWith(Seq("classes", "test").mkString(File.separator)) ) expect(isDefinedTestPathInClassPath) checkIfCompileOutputIsCopied("Tests", tempOutput) } } test("test scope error") { val inputs = TestInputs( os.rel / "Main.scala" -> """object Main { | def message = "Hello from " + "tests" | def main(args: Array[String]): Unit = | println(message) |} |""".stripMargin, os.rel / "Tests.test.scala" -> """//> using dep com.lihaoyi::utest:0.7.10 | |import utest._ | |object Tests extends TestSuite { | val tests = Tests { | test("message") { | pprint.log(Main.message) | assert(Main.message.startsWith("Hello")) | } | } |} |""".stripMargin ) inputs.fromRoot { root => val res = os.proc(TestUtil.cli, "compile", "--test", extraOptions, ".") .call(cwd = root, check = false, stderr = os.Pipe, mergeErrIntoOut = true) expect(res.exitCode == 1) val expectedInOutput = if (actualScalaVersion.startsWith("2.")) "not found: value pprint" else "Not found: pprint" expect(res.out.text().contains(expectedInOutput)) } } test("code in test error") { val inputs = TestInputs( os.rel / "Main.scala" -> """object Main { | def message = "Hello from " + "tests" | def main(args: Array[String]): Unit = { | zz // zz value | println(message) | } |} |""".stripMargin ) inputs.fromRoot { root => val res = os.proc(TestUtil.cli, "compile", extraOptions, ".") .call(cwd = root, check = false, stderr = os.Pipe, mergeErrIntoOut = true) expect(res.exitCode == 1) val expectedInOutput = if (actualScalaVersion.startsWith("2.")) "not found: value zz" else "Not found: zz" val output = res.out.text() expect(output.contains(expectedInOutput)) // errored line should be printed too expect(output.contains("zz // zz value")) if (actualScalaVersion.startsWith("2.12.")) // seems the ranges returned by Bloop / scalac are only one character wide in 2.12 expect(output.contains("^")) else // underline should have length 2 expect(output.contains("^^")) expect(!output.contains("^^^")) } } val jvmT = new munit.Tag("jvm-resolution") val scalaJvm8Project: TestInputs = TestInputs(os.rel / "Main.scala" -> s"object Main{java.util.Optional.of(1).isPresent}") val scalaJvm11Project: TestInputs = TestInputs(os.rel / "Main.scala" -> s"object Main{java.util.Optional.of(1).isEmpty}") val scalaJvm17Project: TestInputs = TestInputs(os.rel / "Main.scala" -> s"object Main{java.util.HexFormat.of().toHexDigits(255)}") val scalaJvm23Project: TestInputs = TestInputs( os.rel / "Main.scala" -> s"object Main{System.out.println(javax.print.attribute.standard.OutputBin.LEFT)}" ) val javaJvm8Project: TestInputs = TestInputs(os.rel / "Main.java" -> """|public class Main{ | public static void main(String[] args) { | java.util.Optional.of(1).isPresent(); | } |}""".stripMargin) val javaJvm11Project: TestInputs = TestInputs(os.rel / "Main.java" -> """|public class Main{ | public static void main(String[] args) { | java.util.Optional.of(1).isEmpty(); | } |}""".stripMargin) val javaJvm17Project: TestInputs = TestInputs(os.rel / "Main.java" -> """|public class Main{ | public static void main(String[] args) { | java.util.HexFormat.of().toHexDigits(255); | } |}""".stripMargin) val javaJvm23Project: TestInputs = TestInputs(os.rel / "Main.java" -> """|public class Main{ | public static void main(String[] args) { | System.out.println(javax.print.attribute.standard.OutputBin.LEFT); | } |}""".stripMargin) def inputs: Map[(String, Int), TestInputs] = if isScala38OrNewer then Map( ("scala", 17) -> scalaJvm17Project, ("scala", 23) -> scalaJvm23Project, ("java", 17) -> javaJvm17Project, ("java", 23) -> javaJvm23Project ) else Map( ("scala", 8) -> scalaJvm8Project, ("scala", 11) -> scalaJvm11Project, ("scala", 17) -> scalaJvm17Project, ("scala", 23) -> scalaJvm23Project, ("java", 8) -> javaJvm8Project, ("java", 11) -> javaJvm11Project, ("java", 17) -> javaJvm17Project, ("java", 23) -> javaJvm23Project ) { val legacyJvms = List(8, 11) val currentJvms = List(17, 23) val jvms = if isScala38OrNewer then currentJvms else legacyJvms ++ currentJvms for { bloopJvm <- jvms targetJvm <- jvms ((lang, sourcesJvm), project) <- inputs } test(s"JvmCompatibilityTest: bloopJvm:$bloopJvm/targetJvm:$targetJvm/lang:$lang/sourcesJvm:$sourcesJvm" .tag(jvmT)) { compileToADifferentJvmThanBloops( bloopJvm.toString, targetJvm.toString, targetJvm >= sourcesJvm, project ) } } test("Scala CLI should not infer scalac --release if --release is passed".tag(jvmT)) { scalaJvm23Project.fromRoot { root => val res = os.proc( TestUtil.cli, "compile", extraOptions, "--jvm", "23", "-release", "17", "." ).call(cwd = root, check = false, stderr = os.Pipe) expect(res.exitCode != 0) val errOutput = res.err.trim() System.err.println(errOutput) expect(errOutput.contains("OutputBin is not a member")) expect(errOutput.contains( "Warning: different target JVM (23) and scala compiler target JVM (17) were passed." )) } } if (actualScalaVersion.startsWith("2.1")) test("warn for different target JVMs in --jvm, -target:x and -release".tag(jvmT)) { scalaJvm8Project.fromRoot { root => val res = os.proc( TestUtil.cli, "compile", extraOptions, "--jvm", "11", "-release", "8", "-target:8", "." ).call(cwd = root, check = false, stderr = os.Pipe) expect(res.exitCode == 0) val errOutput = res.err.trim() expect(errOutput.contains( "Warning: different target JVM (11) and scala compiler target JVM (8) were passed." )) } } def compileToADifferentJvmThanBloops( bloopJvm: String, targetJvm: String, shouldSucceed: Boolean, inputs: TestInputs ): Unit = inputs.fromRoot { root => val bloop = BloopUtil.bloop(Constants.bloopVersion, bloopDaemonDir, jvm = Some(bloopJvm)) bloop(Seq("exit")).call( cwd = root, check = false, stdout = os.Inherit ) bloop(Seq("about")).call( cwd = root, check = false, stdout = os.Inherit ) val res = os.proc(TestUtil.cli, "compile", extraOptions, "--jvm", targetJvm, ".") .call(cwd = root, check = false, stderr = os.Pipe) val succeeded = res.exitCode == 0 if succeeded != shouldSucceed then System.err.println(res.err.text()) expect(succeeded == shouldSucceed) if !shouldSucceed then expect( res.err.text().contains("is not a member") || res.err.text().contains("cannot find symbol") ) } if (actualScalaVersion.startsWith("2.12")) test("JVM options only for JVM platform") { val inputs = TestInputs(os.rel / "Main.scala" -> "//> using `java-opt` \"-Xss1g\"") inputs.fromRoot { root => val res = os.proc(TestUtil.cli, "compile", extraOptions, "--native", ".").call( cwd = root, stderr = os.Pipe ) val stderr = res.err.text() expect(s"\\[.*warn.*].*Conflicting options.*".r.findFirstMatchIn(stderr).isDefined) } } if (actualScalaVersion.startsWith("3")) test("generate scoverage.coverage file") { val fileName = "Hello.scala" val inputs = TestInputs( os.rel / fileName -> s"""//> using options -coverage-out:. | |@main def main = () |""".stripMargin ) inputs.fromRoot { root => os.proc(TestUtil.cli, "compile", extraOptions, fileName) .call(cwd = root) .out.trim() val expectedCoverageFilePath = root / "scoverage.coverage" expect(os.exists(expectedCoverageFilePath)) } } if (actualScalaVersion.startsWith("2.")) test("no duplicates in class path") { noDuplicatesInClassPathTest() } def noDuplicatesInClassPathTest(): Unit = { val sparkVersion = "3.3.0" val inputs = TestInputs( os.rel / "Hello.scala" -> s"""//> using dep org.apache.spark::spark-sql:$sparkVersion |object Hello { | def main(args: Array[String]): Unit = | println("Hello") |} |""".stripMargin ) inputs.fromRoot { root => val res = os.proc( TestUtil.cli, "compile", "--print-class-path", extraOptions, "." ).call(cwd = root) val classPath = res.out.trim().split(File.pathSeparator) val classPathFileNames = classPath.map(_.split("[\\\\/]+").last) expect(classPathFileNames.exists(_.startsWith("spark-core_"))) // usually a duplicate is there if we don't call .distrinct when necessary here or there expect(classPathFileNames.exists(_.startsWith("snappy-java"))) val duplicates = classPath.groupBy(identity).view.mapValues(_.length).filter(_._2 > 1).toVector expect(duplicates.isEmpty) } } test("override settings from tests") { val olderJava = Constants.scala38MinJavaVersion.toString val newerJava = Constants.allJavaVersions.max.toString val inputs = TestInputs( os.rel / "MainStuff.scala" -> s"""//> using jvm $olderJava |object MainStuff { | def javaVer = sys.props("java.version") | def main(args: Array[String]): Unit = { | println(s"Found Java $$javaVer in main scope") | assert(javaVer == "$olderJava" || javaVer.startsWith("$olderJava.")) | } |} |""".stripMargin, os.rel / "TestStuff.test.scala" -> s"""//> using jvm $newerJava |//> using dep org.scalameta::munit:0.7.29 |class TestStuff extends munit.FunSuite { | test("the test") { | val javaVer = MainStuff.javaVer | println(s"Found Java $$javaVer in test scope") | val javaVer0 = { | val bais = new java.io.ByteArrayInputStream(javaVer.getBytes("UTF-8")) | new String(bais.readAllBytes(), "UTF-8") // readAllBytes available only on Java 17 (not on Java 8) | } | assert(javaVer0 == "$newerJava" || javaVer0.startsWith("$newerJava.")) | } |} |""".stripMargin ) inputs.fromRoot { root => os.proc(TestUtil.cli, "compile", "--test", ".") .call(cwd = root, stdin = os.Inherit, stdout = os.Inherit) os.proc(TestUtil.cli, "run", ".") .call(cwd = root, stdin = os.Inherit, stdout = os.Inherit) os.proc(TestUtil.cli, "test", ".") .call(cwd = root, stdin = os.Inherit, stdout = os.Inherit) } } test("scalapy") { def maybeScalapyPrefix = if (actualScalaVersion.startsWith("2.13.")) "" else "import me.shadaj.scalapy.py" + System.lineSeparator() val inputs = TestInputs( os.rel / "Hello.scala" -> s"""$maybeScalapyPrefix |object Hello { | def main(args: Array[String]): Unit = { | py.Dynamic.global.print("Hello from Python", flush = true) | } |} |""".stripMargin ) inputs.fromRoot { root => val res = os.proc( TestUtil.cli, "--power", "compile", "--python", "--print-class-path", ".", extraOptions ) .call(cwd = root) val classPath = res.out.trim().split(File.pathSeparator) val outputDir = os.Path(classPath.head, root) val classFiles = os.walk(outputDir) .filter(_.last.endsWith(".class")) .filter(os.isFile(_)) .map(_.relativeTo(outputDir)) val path = os.rel / "Hello.class" expect(classFiles.contains(path)) } } private def compilerArtifactName: String = if (actualScalaVersion.startsWith("3")) "scala3-compiler" else "scala-compiler" test(s"ensure the -with-compiler option adds $compilerArtifactName to the classpath") { TestInputs(os.rel / "s.sc" -> """println("Hello")""") .fromRoot { root => val compileRes = os.proc( TestUtil.cli, "compile", "s.sc", "--print-classpath", extraOptions ) .call(cwd = root) expect(!compileRes.out.trim().contains(compilerArtifactName)) val compileWithCompilerRes = os.proc( TestUtil.cli, "compile", "s.sc", "-with-compiler", "--print-classpath", extraOptions ) .call(cwd = root) expect(compileWithCompilerRes.out.trim().contains(compilerArtifactName)) } } test(s"reuse cached project file under .scala-build") { TestInputs(os.rel / "main.scala" -> """object Main extends App { | println("Hello") |}""".stripMargin) .fromRoot { root => val firstRes = os.proc( TestUtil.cli, "compile", "main.scala" ).call(cwd = root, mergeErrIntoOut = true) expect(os.list(root / Constants.workspaceDirName).count( _.baseName.startsWith(root.baseName) ) == 1) val firstOutput = TestUtil.normalizeConsoleOutput(firstRes.out.text()) expect(firstOutput.contains("Compiled project")) val differentRes = os.proc( TestUtil.cli, "compile", "main.scala", "--jvm", Constants.scala38MinJavaVersion.toString ).call(cwd = root, mergeErrIntoOut = true) expect(os.list(root / Constants.workspaceDirName).count( _.baseName.startsWith(root.baseName) ) == 2) val differentOutput = TestUtil.normalizeConsoleOutput(differentRes.out.text()) expect(differentOutput.contains("Compiled project")) val secondRes = os.proc( TestUtil.cli, "compile", "main.scala" ).call(cwd = root, mergeErrIntoOut = true) expect(os.list(root / Constants.workspaceDirName).count( _.baseName.startsWith(root.baseName) ) == 2) val secondOutput = TestUtil.normalizeConsoleOutput(secondRes.out.text()) expect(!secondOutput.contains("Compiled project")) } } test( "pass java options to scalac when server=false (Scala-only, no .java-not-compiled warning)" ) { val inputs = TestInputs( os.rel / "Main.scala" -> """object Main extends App { | println("Hello") |} |""".stripMargin ) inputs.fromRoot { root => val res = os.proc( TestUtil.cli, "compile", "--scalac-option=-J-XX:MaxHeapSize=1k", "--server=false", extraOptions, "." ) .call(cwd = root, check = false, mergeErrIntoOut = true) expect(res.exitCode == 1) val out = res.out.text() expect(out.contains("Error occurred during initialization of VM")) expect(out.contains("Too small maximum heap")) expect(!out.contains(".java files are not compiled to .class files")) } } test("new build targets should only be created when CLI options change") { val filename = "Main.scala" val inputs = TestInputs( os.rel / filename -> """object Main extends App { | println("Hello") |} |""".stripMargin, os.rel / "Test.test.scala" -> """object Test extends App { | println("Hello") |} |""".stripMargin ) inputs.fromRoot { root => os.proc(TestUtil.cli, "compile", extraOptions :+ "--test", ".").call(cwd = root) def buildTargetDirs = os.list(root / Constants.workspaceDirName) .filter(os.isDir) .filter(_.last != ".bloop") expect(buildTargetDirs.size == 1) os.write.over( root / filename, """//> using dep com.lihaoyi::os-lib:0.9.1 | |object Main extends App { | println("Hello") |} |""".stripMargin ) os.proc(TestUtil.cli, "compile", extraOptions :+ "--test", ".").call(cwd = root) expect(buildTargetDirs.size == 1) os.proc(TestUtil.cli, "compile", extraOptions ++ Seq("--test", "-nowarn"), ".").call(cwd = root ) expect(buildTargetDirs.size == 2) } } if (!Properties.isWin) // TODO: make this work on Windows: https://github.com/VirtusLab/scala-cli/issues/2973 test( "nested wildcard path source exclusion with a directive and no special character escaping" ) { val excludedFileName = "Foo.scala" val excludedPath = os.rel / "dir1" / "dir2" / excludedFileName val inputs = TestInputs( os.rel / "project.scala" -> s"//> using exclude */*/$excludedFileName", excludedPath -> "val foo // invalid code" ) inputs.fromRoot { root => os.proc(TestUtil.cli, "compile", extraOptions, ".").call(cwd = root) } } test("no previous compilation error should be printed") { val filename = "Main.scala" val inputs = TestInputs( os.rel / filename -> """|object Main { | val msg: String = "1" |} |""".stripMargin ) inputs.fromRoot { root => val result = os.proc(TestUtil.cli, "compile", ".", extraOptions).call( cwd = root, check = false, mergeErrIntoOut = true ) val jvmVersion = Constants.defaultGraalVMJavaVersion val expectedOutput = s"""|Compiling project (Scala $actualScalaVersion, JVM ($jvmVersion)) |Compiled project (Scala $actualScalaVersion, JVM ($jvmVersion))""".stripMargin val actualOutput = TestUtil.fullStableOutput(result) assertEquals(actualOutput, expectedOutput) os.write.over( root / filename, """|object Main { | val msg: String = 1 |} |""".stripMargin ) val result2 = os.proc(TestUtil.cli, "compile", ".", extraOptions).call( cwd = root, check = false, mergeErrIntoOut = true ) val expectedError = if actualScalaVersion.startsWith("2") then """|[error] type mismatch; |[error] found : Int(1) |[error] required: String""".stripMargin else """|[error] Found: (1 : Int) |[error] Required: String""".stripMargin val actualOutput2 = TestUtil.fullStableOutput(result2).trim val expectedOutput2 = s"""|Compiling project (Scala $actualScalaVersion, JVM ($jvmVersion)) |[error] .${File.separatorChar}Main.scala:2:23 |$expectedError |[error] val msg: String = 1 |[error] ^ |Error compiling project (Scala $actualScalaVersion, JVM ($jvmVersion)) |Compilation failed""".stripMargin assertEquals(actualOutput2, expectedOutput2) os.write.over( root / filename, """|object Main { | val msg: String = "1" |} |""".stripMargin ) val result3 = os.proc(TestUtil.cli, "compile", ".", extraOptions).call( cwd = root, check = false, mergeErrIntoOut = true ) val actualOutput3 = TestUtil.fullStableOutput(result3) val expectedOutput3 = s"""|Compiling project (Scala $actualScalaVersion, JVM ($jvmVersion)) |Compiled project (Scala $actualScalaVersion, JVM ($jvmVersion))""".stripMargin assertEquals(actualOutput3, expectedOutput3) } } test("i3389") { val filename = "Main.scala" val inputs = TestInputs( os.rel / filename -> """//> using optionsdeprecation |""".stripMargin ) inputs.fromRoot { root => val result = os.proc(TestUtil.cli, "compile", ".", extraOptions).call( cwd = root, check = false, mergeErrIntoOut = true ) assertEquals( TestUtil.fullStableOutput(result).trim(), s"""|[error] .${File.separatorChar}Main.scala:1:11 |[error] Unrecognized directive: optionsdeprecation |[error] //> using optionsdeprecation |[error] ^^^^^^^^^^^^^^^^^^""".stripMargin ) } } test("i3389-2") { val filename = "Main.scala" val inputs = TestInputs( os.rel / filename -> """//> using unrecognised.directive value1 value2 |""".stripMargin ) inputs.fromRoot { root => val result = os.proc(TestUtil.cli, "compile", ".", extraOptions).call( cwd = root, check = false, mergeErrIntoOut = true ) assertEquals( TestUtil.fullStableOutput(result).trim(), s"""|[error] .${File.separatorChar}Main.scala:1:11 |[error] Unrecognized directive: unrecognised.directive with values: value1, value2 |[error] //> using unrecognised.directive value1 value2 |[error] ^^^^^^^^^^^^^^^^^^^^^^""".stripMargin ) } } if (!Properties.isMac || !TestUtil.isCI) test("--watching with --watch re-compiles on external file change") { val sourceFile = os.rel / "Main.scala" val externalFile = os.rel / "data" / "input.txt" TestInputs( sourceFile -> """object Main { | def value = 1 |} |""".stripMargin, externalFile -> "Hello" ).fromRoot { root => TestUtil.withProcessWatching( proc = os.proc( TestUtil.cli, "--power", "compile", ".", "--watch", "--watching", "data", extraOptions ) .spawn(cwd = root, stderr = os.Pipe), timeout = 120.seconds ) { (proc, timeout, ec) => implicit val ec0 = ec val initialOutput = proc.readStderrUntilWatchingMessage(timeout) expect(initialOutput.exists(_.contains("Compiled"))) Thread.sleep(2000L) os.write.over(root / externalFile, "World") val rerunOutput = proc.readStderrUntilWatchingMessage(timeout) expect(rerunOutput.nonEmpty) } } } test("sbt file in directory does not break compile") { TestInputs( os.rel / "Main.scala" -> """object Main { | def main(args: Array[String]): Unit = println("Hello") |} |""".stripMargin, os.rel / "build.sbt" -> """name := "my-project"""" ).fromRoot { root => os.proc(TestUtil.cli, "compile", extraOptions, ".").call(cwd = root) } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/CompileTests212.scala ================================================ package scala.cli.integration class CompileTests212 extends CompileTestDefinitions with Test212 { val pluginInputs: TestInputs = TestInputs( os.rel / "Plugin.scala" -> // Copied from (https://github.com/typelevel/kind-projector/blob/00bf25cef1b7d01d61a3555cccb6cf38fe30e117/src/test/scala/polylambda.scala) """object Plugin { | trait ~>[-F[_], +G[_]] { | def apply[A](x: F[A]): G[A] | } | type ToSelf[F[_]] = F ~> F | val kf5 = λ[Map[*, Int] ~> Map[*, Long]](_.map { case (k, v) => (k, v.toLong) }.toMap) | val kf6 = λ[ToSelf[Map[*, Int]]](_.map { case (k, v) => (k, v * 2) }.toMap) |} |""".stripMargin ) val kindProjectPlugin = "org.typelevel:::kind-projector:0.13.4" test("should compile with compiler plugin") { pluginInputs.fromRoot { root => os.proc( TestUtil.cli, "compile", extraOptions, ".", "--compiler-plugin", kindProjectPlugin ).call(cwd = root).out.text() } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/CompileTests213.scala ================================================ package scala.cli.integration import scala.util.Properties class CompileTests213 extends CompileTestDefinitions with Test213 { test("test-macro-output") { val triple = "\"\"\"" TestInputs( os.rel / "Main.scala" -> s"""|//> using scala ${Constants.scala213} |//> using dep org.scala-lang:scala-reflect:${Constants.scala213} |package example |import scala.reflect.macros.blackbox |import scala.language.experimental.macros | |object Scala2Example { | def macroMethod[A](a: A): String = | macro Scala2Example.macroMethodImpl[A] | | def macroMethodImpl[A: c.WeakTypeTag]( | c: blackbox.Context | )(a: c.Expr[A]): c.Expr[String] = { | import c.universe._ | val output = s$triple$${show(a.tree)} | |$${showCode(a.tree)} | |$${showRaw(a.tree)} | |$${weakTypeTag[A]} | |$${weakTypeOf[A]} | |$${showRaw(weakTypeOf[A])}$triple.stripMargin | c.echo(c.enclosingPosition, output) | c.warning(c.enclosingPosition, "example error message") | c.abort(c.enclosingPosition, "example error message") | } |} |""".stripMargin, os.rel / "Test.test.scala" -> """|//> using test.dep org.scalameta::munit::1.0.0 |package example | |class Tests extends munit.FunSuite { | test("macro works OK") { | Scala2Example.macroMethod(1 -> "test") | } |}""".stripMargin ).fromRoot { root => val result = os.proc(TestUtil.cli, "test", ".", extraOptions).call( cwd = root, check = false, mergeErrIntoOut = true ) val separator = if (Properties.isWin) "\\" else "/" val graalVmVersion = Constants.defaultGraalVMJavaVersion val legacyRunnerVersion = Constants.runnerScala2LegacyVersion val ltsPrefix = Constants.scala3LtsPrefix val expectedOutput = s"""|Compiling project (Scala $actualScalaVersion, JVM ($graalVmVersion)) |Compiled project (Scala $actualScalaVersion, JVM ($graalVmVersion)) |[warn] Scala $actualScalaVersion is no longer supported by the test-runner module. |[warn] Defaulting to a legacy test-runner module version: $legacyRunnerVersion. |[warn] To use the latest test-runner, upgrade Scala to at least $ltsPrefix. |Compiling project (test, Scala $actualScalaVersion, JVM ($graalVmVersion)) |[info] .${separator}Test.test.scala:6:5 |[info] scala.Predef.ArrowAssoc[Int](1).->[String]("test") |[info] scala.Predef.ArrowAssoc[Int](1).->[String]("test") |[info] Apply(TypeApply(Select(Apply(TypeApply(Select(Select(Ident(scala), scala.Predef), TermName("ArrowAssoc")), List(TypeTree())), List(Literal(Constant(1)))), TermName("$$minus$$greater")), List(TypeTree())), List(Literal(Constant("test")))) |[info] WeakTypeTag[(Int, String)] |[info] (Int, String) |[info] TypeRef(ThisType(scala), scala.Tuple2, List(TypeRef(ThisType(scala), scala.Int, List()), TypeRef(ThisType(java.lang), java.lang.String, List()))) |[info] Scala2Example.macroMethod(1 -> "test") |[info] ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |[error] .${separator}Test.test.scala:6:5 |[error] example error message |[error] Scala2Example.macroMethod(1 -> "test") |[error] ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |Error compiling project (test, Scala $actualScalaVersion, JVM ($graalVmVersion)) |Compilation failed |""".stripMargin assertNoDiff( TestUtil.fullStableOutput(result), expectedOutput ) } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/CompileTests3Lts.scala ================================================ package scala.cli.integration class CompileTests3Lts extends CompileTestDefinitions with CompileTests3StableDefinitions with Test3Lts ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/CompileTests3NextRc.scala ================================================ package scala.cli.integration class CompileTests3NextRc extends CompileTestDefinitions with Test3NextRc ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/CompileTests3StableDefinitions.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect import java.io.File trait CompileTests3StableDefinitions { this: CompileTestDefinitions => test(s"TASTY processor does not warn about Scala $actualScalaVersion") { TestInputs(os.rel / "simple.sc" -> s"""println("Hello")""") .fromRoot { root => val result = os.proc(TestUtil.cli, "compile", ".", extraOptions) .call(cwd = root, stderr = os.Pipe) expect(result.exitCode == 0) expect(!result.err.text().contains("cannot post process TASTY files")) } } test("render explain message") { val fileName = "Hello.scala" val inputs = TestInputs( os.rel / fileName -> // should be dump to 3.3.1 after release s"""//> using scala 3.3.1-RC1-bin-20230203-3ef1e73-NIGHTLY |//> using options --explain | |class A |val i: Int = A() |""".stripMargin ) inputs.fromRoot { root => val out = os.proc(TestUtil.cli, "compile", extraOptions, fileName) .call(cwd = root, check = false, mergeErrIntoOut = true).out.trim() expect(out.contains("Explanation")) } } test("as jar") { val inputs = TestInputs( os.rel / "Foo.scala" -> """object Foo { | def n = 2 |} |""".stripMargin ) inputs.fromRoot { root => val out = os.proc(TestUtil.cli, "compile", extraOptions, ".", "--print-class-path") .call(cwd = root) .out.trim() val cp = out.split(File.pathSeparator).toVector.map(os.Path(_, root)) expect(cp.headOption.exists(os.isDir(_))) expect(cp.drop(1).forall(os.isFile(_))) val asJarOut = os.proc( TestUtil.cli, "--power", "compile", extraOptions, ".", "--print-class-path", "--as-jar" ) .call(cwd = root) .out.trim() val asJarCp = asJarOut.split(File.pathSeparator).toVector.map(os.Path(_, root)) expect(asJarCp.forall(os.isFile(_))) } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/CompileTestsDefault.scala ================================================ package scala.cli.integration class CompileTestsDefault extends CompileTestDefinitions with CompileTests3StableDefinitions with TestDefault { test( s"compile --cross $actualScalaVersion with ${Constants.scala213} and ${Constants.scala212}" ) { val crossVersions = Seq(actualScalaVersion, Constants.scala213, Constants.scala212) simpleInputs .add(os.rel / "project.scala" -> s"//> using scala ${crossVersions.mkString(" ")}") .fromRoot { root => os.proc(TestUtil.cli, "compile", ".", "--cross", "--power", extraOptions).call(cwd = root) } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/CompilerPluginTestDefinitions.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect import scala.cli.integration.util.CompilerPluginUtil trait CompilerPluginTestDefinitions { this: CompileTestDefinitions => def compilerPluginInputs(pluginName: String, pluginErrorMsg: String): TestInputs = if (actualScalaVersion.startsWith("3")) CompilerPluginUtil.compilerPluginForScala3(pluginName, pluginErrorMsg) else CompilerPluginUtil.compilerPluginForScala2(pluginName, pluginErrorMsg) for { pluginViaDirective <- Seq(true, false) testLabel = if (pluginViaDirective) "use plugin via directive" else "use plugin via CLI option" } test(s"build a custom compiler plugin and use it ($testLabel)") { val pluginName = "divbyzero" val usePluginFile = "Main.scala" val outputJar = "div-by-zero.jar" val pluginErrorMsg = "definitely division by zero" compilerPluginInputs(pluginName, pluginErrorMsg) .add(os.rel / usePluginFile -> s"""${if (pluginViaDirective) s"//> using option -Xplugin:$outputJar" else ""} | |object Test { | val five = 5 | val amount = five / 0 | def main(args: Array[String]): Unit = { | println(amount) | } |} |""".stripMargin) .fromRoot { root => // build the compiler plugin os.proc( TestUtil.cli, "package", s"$pluginName.scala", "--power", "--with-compiler", "--library", "-o", outputJar, extraOptions ).call(cwd = root) expect(os.isFile(root / outputJar)) // verify the plugin is loaded val pluginListResult = os.proc( TestUtil.cli, "compile", s"-Xplugin:$outputJar", "-Xplugin-list", extraOptions ).call(cwd = root, mergeErrIntoOut = true) expect(pluginListResult.out.text().contains(pluginName)) // verify the compiler plugin phase is being added correctly os.proc( TestUtil.cli, "compile", s"-Xplugin:$outputJar", "-Xshow-phases", extraOptions ).call(cwd = root, mergeErrIntoOut = true) expect(pluginListResult.out.text().contains(pluginName)) val pluginOptions = if (pluginViaDirective) Nil else Seq(s"-Xplugin:$outputJar") // verify the compiler plugin is working // TODO: this shouldn't require running with --server=false val res = os.proc( TestUtil.cli, "compile", pluginOptions, usePluginFile, "--server=false", extraOptions ) .call(cwd = root, mergeErrIntoOut = true, check = false) expect(res.exitCode == 1) expect(res.out.text().contains(pluginErrorMsg)) } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/CompleteTests.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect import scala.util.Properties class CompleteTests extends ScalaCliSuite { test("simple") { TestInputs.empty.fromRoot { root => val res = os.proc(TestUtil.cli, "complete", shellFormat, "2", "com").call(cwd = root) expect(res.exitCode == 0) expect(res.out.trim().nonEmpty) } } test("zsh bug") { // guard against https://github.com/alexarchambault/case-app/issues/475 TestInputs.empty.fromRoot { root => val res = os.proc(TestUtil.cli, "complete", shellFormat, "2", "scala-cli").call(cwd = root) expect(res.exitCode == 0) expect(!res.out.text().contains(raw"\'")) } } def shellFormat: String = if (Properties.isMac) "zsh-v1" else "bash-v1" } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/ConfigTests.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect import java.io.File import scala.util.Properties class ConfigTests extends ScalaCliSuite { override def group: ScalaCliSuite.TestGroup = ScalaCliSuite.TestGroup.First test("simple") { val configFile = os.rel / "config" / "config.json" val configEnv = Map("SCALA_CLI_CONFIG" -> configFile.toString()) val name = "Alex" TestInputs.empty.fromRoot { root => val before = // Test --power placed after subcommand name os.proc(TestUtil.cli, "config", "publish.user.name", "--power").call( cwd = root, env = configEnv ) expect(before.out.trim().isEmpty) os.proc(TestUtil.cli, "config", "publish.user.name", name, "--power").call( cwd = root, env = configEnv ) val res = os.proc(TestUtil.cli, "config", "publish.user.name", "--power").call( cwd = root, env = configEnv ) expect(res.out.trim() == name) os.proc(TestUtil.cli, "config", "publish.user.name", "--unset", "--power").call( cwd = root, env = configEnv ) val after = os.proc(TestUtil.cli, "config", "publish.user.name", "--power").call( cwd = root, env = configEnv ) expect(after.out.trim().isEmpty) } } test("password") { val configFile = os.rel / "config" / "config.json" val configEnv = Map("SCALA_CLI_CONFIG" -> configFile.toString) val password = "1234" val key = "httpProxy.password" TestInputs.empty.fromRoot { root => def emptyCheck(): Unit = { val value = os.proc(TestUtil.cli, "--power", "config", key) .call(cwd = root, env = configEnv) expect(value.out.trim().isEmpty) } def unset(): Unit = os.proc(TestUtil.cli, "--power", "config", key, "--unset") .call(cwd = root, env = configEnv) def read(): String = { val res = os.proc(TestUtil.cli, "--power", "config", key) .call(cwd = root, env = configEnv) res.out.trim() } def readDecoded(env: Map[String, String] = Map.empty): String = { val res = os.proc(TestUtil.cli, "--power", "config", key, "--password-value") .call(cwd = root, env = configEnv ++ env) res.out.trim() } emptyCheck() os.proc(TestUtil.cli, "--power", "config", key, s"value:$password") .call(cwd = root, env = configEnv) expect(read() == s"value:$password") expect(readDecoded() == password) unset() emptyCheck() os.proc(TestUtil.cli, "--power", "config", key, "env:MY_PASSWORD") .call(cwd = root, env = configEnv) expect(read() == "env:MY_PASSWORD") expect(readDecoded(env = Map("MY_PASSWORD" -> password)) == password) unset() emptyCheck() os.proc( TestUtil.cli, "--power", "config", key, "env:MY_PASSWORD", "--password-value" ) .call(cwd = root, env = Map("MY_PASSWORD" -> password) ++ configEnv) expect(read() == s"value:$password") expect(readDecoded() == password) unset() emptyCheck() } } test("Respect SCALA_CLI_CONFIG and format on write") { val proxyAddr = "https://foo.bar.com" TestInputs().fromRoot { root => val confDir = root / "config" val confFile = confDir / "test-config.json" val content = // non-formatted on purpose s"""{ | "httpProxy": { "address" : "$proxyAddr" } } |""".stripMargin os.write(confFile, content, createFolders = true) if (!Properties.isWin) os.perms.set(confDir, "rwx------") val extraEnv = Map("SCALA_CLI_CONFIG" -> confFile.toString) ++ TestUtil.putCsInPathViaEnv(root / "bin") val res = os.proc(TestUtil.cli, "--power", "config", "httpProxy.address") .call(cwd = root, env = extraEnv) val value = res.out.trim() expect(value == proxyAddr) os.proc(TestUtil.cli, "config", "interactive", "false") .call(cwd = root, env = extraEnv) val expectedUpdatedContent = // too many spaces after some ':' (jsoniter-scala bug?) s"""{ | "httpProxy": { | "address": "https://foo.bar.com" | }, | "interactive": false |} |""".stripMargin.replace("\r\n", "\n") val updatedContent = os.read(confFile) expect(updatedContent == expectedUpdatedContent) } } if (!Properties.isWin) test("Exit with non-zero error code if saving failed") { nonZeroErrorCodeOnFailedSaveTest() } def nonZeroErrorCodeOnFailedSaveTest(): Unit = { val proxyAddr = "https://foo.bar.com" TestInputs().fromRoot { root => val confDir = root / "config" os.makeDir.all(confDir) // not adjusting perms - should make things fail below val confFile = confDir / "test-config.json" val extraEnv = Map("SCALA_CLI_CONFIG" -> confFile.toString) val res = os.proc(TestUtil.cli, "--power", "config", "httpProxy.address", proxyAddr) .call(cwd = root, env = extraEnv, check = false, mergeErrIntoOut = true) val output = res.out.trim() expect(output.contains(" has wrong permissions")) } } if (!TestUtil.isCI || !Properties.isWin) for (pgpPasswordOption <- List("none", "random", "MY_CHOSEN_PASSWORD")) test(s"Create a default PGP key, password: $pgpPasswordOption") { createDefaultPgpKeyTest(pgpPasswordOption) } if (TestUtil.isNativeCli) test(s"Create a PGP key with external JVM process, java version too low") { TestUtil.retryOnCi() { TestInputs().fromRoot { root => val configFile = { val dir = root / "config" os.makeDir.all(dir, perms = if (Properties.isWin) null else "rwx------") dir / "config.json" } val java8Home = os.Path(os.proc(TestUtil.cs, "java-home", "--jvm", "zulu:8").call().out.trim(), os.pwd) val extraEnv = Map( "JAVA_HOME" -> java8Home.toString, "PATH" -> ((java8Home / "bin").toString + File.pathSeparator + System.getenv("PATH")), "SCALA_CLI_CONFIG" -> configFile.toString ) val pgpCreated = os.proc( TestUtil.cli, "--power", "config", "--create-pgp-key", "--email", "alex@alex.me", "--pgp-password", "none", "--force-jvm-signing-cli", "-v", "-v", "-v" ) .call(cwd = root, env = extraEnv, mergeErrIntoOut = true) val javaCommandLine = pgpCreated.out.text() .linesIterator .dropWhile(!_.equals(" Running")).slice(1, 2) .toSeq expect(javaCommandLine.nonEmpty) expect(javaCommandLine.head.contains("17")) val passwordInConfig = os.proc(TestUtil.cli, "--power", "config", "pgp.secret-key-password") .call(cwd = root, env = extraEnv, stderr = os.Pipe) expect(passwordInConfig.out.text().isEmpty()) val secretKey = os.proc(TestUtil.cli, "--power", "config", "pgp.secret-key") .call(cwd = root, env = extraEnv, stderr = os.Pipe) .out.trim() val rawPublicKey = os.proc(TestUtil.cli, "--power", "config", "pgp.public-key", "--password-value") .call(cwd = root, env = extraEnv, stderr = os.Pipe) .out.trim() val tmpFile = root / "test-file" val tmpFileAsc = root / "test-file.asc" os.write(tmpFile, "Hello") val q = "\"" def maybeEscape(arg: String): String = if (Properties.isWin) q + arg + q else arg os.proc( TestUtil.cli, "--power", "pgp", "sign", "--secret-key", maybeEscape(secretKey), tmpFile ).call(cwd = root, stdin = os.Inherit, stdout = os.Inherit, env = extraEnv) val pubKeyFile = root / "key.pub" os.write(pubKeyFile, rawPublicKey) val verifyResult = os.proc(TestUtil.cli, "--power", "pgp", "verify", "--key", pubKeyFile, tmpFileAsc) .call(cwd = root, env = extraEnv, mergeErrIntoOut = true) expect(verifyResult.out.text().contains("valid signature")) } } } def createDefaultPgpKeyTest(pgpPasswordOption: String): Unit = { TestInputs().fromRoot { root => val configFile = { val dir = root / "config" os.makeDir.all(dir, perms = if (Properties.isWin) null else "rwx------") dir / "config.json" } val extraEnv = Map( "SCALA_CLI_CONFIG" -> configFile.toString ) val checkPassword = os.proc(TestUtil.cli, "--power", "config", "--create-pgp-key") .call(cwd = root, env = extraEnv, check = false, mergeErrIntoOut = true) expect(checkPassword.exitCode != 0) expect(checkPassword.out.text().contains("--pgp-password")) val checkEmail = os.proc( TestUtil.cli, "--power", "config", "--create-pgp-key", "--pgp-password", pgpPasswordOption ) .call(cwd = root, env = extraEnv, check = false, mergeErrIntoOut = true) expect(checkEmail.exitCode != 0) expect(checkEmail.out.text().contains("--email")) val pgpCreated = os.proc( TestUtil.cli, "--power", "config", "--create-pgp-key", "--email", "alex@alex.me", "--pgp-password", pgpPasswordOption ) .call(cwd = root, env = extraEnv, mergeErrIntoOut = true) val pgpPasswordOpt: Option[String] = pgpCreated.out.text() .linesIterator .toSeq .find(_.startsWith("Password")) .map(_.stripPrefix("Password:").trim()) if (pgpPasswordOption != "random") expect(pgpPasswordOpt.isEmpty) else expect(pgpPasswordOpt.isDefined) val passwordInConfig = os.proc(TestUtil.cli, "--power", "config", "pgp.secret-key-password") .call(cwd = root, env = extraEnv, stderr = os.Pipe) expect(passwordInConfig.out.text().isEmpty()) val secretKey = os.proc(TestUtil.cli, "--power", "config", "pgp.secret-key") .call(cwd = root, env = extraEnv, stderr = os.Pipe) .out.trim() val rawPublicKey = os.proc(TestUtil.cli, "--power", "config", "pgp.public-key", "--password-value") .call(cwd = root, env = extraEnv, stderr = os.Pipe) .out.trim() val tmpFile = root / "test-file" val tmpFileAsc = root / "test-file.asc" os.write(tmpFile, "Hello") val q = "\"" def maybeEscape(arg: String): String = if (Properties.isWin) q + arg + q else arg val signProcess = if (pgpPasswordOption != "none") os.proc( TestUtil.cli, "--power", "pgp", "sign", "--password", s"value:${maybeEscape(pgpPasswordOpt.getOrElse("MY_CHOSEN_PASSWORD"))}", "--secret-key", maybeEscape(secretKey), tmpFile ) else os.proc( TestUtil.cli, "--power", "pgp", "sign", "--secret-key", maybeEscape(secretKey), tmpFile ) signProcess.call(cwd = root, stdin = os.Inherit, stdout = os.Inherit, env = extraEnv) val pubKeyFile = root / "key.pub" os.write(pubKeyFile, rawPublicKey) val verifyResult = os.proc(TestUtil.cli, "--power", "pgp", "verify", "--key", pubKeyFile, tmpFileAsc) .call(cwd = root, env = extraEnv, mergeErrIntoOut = true) expect(verifyResult.out.text().contains("valid signature")) } } test("repository credentials") { val testOrg = "test-org" val testName = "the-messages" val testVersion = "0.1.2" val user = "alex" val password = "1234" val realm = "LeTestRealm" val inputs = TestInputs( os.rel / "messages" / "Messages.scala" -> """package messages | |object Messages { | def hello(name: String): String = | s"Hello $name" |} |""".stripMargin, os.rel / "hello" / "Hello.scala" -> s"""//> using dep $testOrg::$testName:$testVersion |import messages.Messages |object Hello { | def main(args: Array[String]): Unit = | println(Messages.hello(args.headOption.getOrElse("Unknown"))) |} |""".stripMargin ) inputs.fromRoot { root => val configFile = { val dir = root / "conf" os.makeDir.all(dir, if (Properties.isWin) null else "rwx------") dir / "config.json" } val extraEnv = Map( "SCALA_CLI_CONFIG" -> configFile.toString ) val repoPath = root / "the-repo" os.proc( TestUtil.cli, "--power", "publish", "--publish-repo", repoPath.toNIO.toUri.toASCIIString, "messages", "--organization", testOrg, "--name", testName, "--project-version", testVersion ) .call(cwd = root, stdin = os.Inherit, stdout = os.Inherit, env = extraEnv) TestUtil.serveFilesInHttpServer(repoPath, user, password, realm) { (host, port) => os.proc( TestUtil.cli, "--power", "config", "repositories.credentials", host, s"value:$user", s"value:$password", realm ) .call(cwd = root, stdin = os.Inherit, stdout = os.Inherit, env = extraEnv) val credentialsAsStringRes = os.proc( TestUtil.cli, "--power", "config", "repositories.credentials" ).call(cwd = root, env = extraEnv) val linePrefix = "configRepo0" val expectedCredentialsAsString = s"""$linePrefix.host=$host |$linePrefix.username=value:$user |$linePrefix.password=value:$password |$linePrefix.realm=$realm |$linePrefix.auto=true""".stripMargin expect(credentialsAsStringRes.out.trim() == expectedCredentialsAsString) val res = os.proc( TestUtil.cli, "run", "--repository", s"http://$host:$port", "hello", "--", "TestUser" ) .call(cwd = root, env = extraEnv) val output = res.out.trim() expect(output == "Hello TestUser") } } } test("password-value in credentials") { val configFile = os.rel / "config" / "config.json" val passwordEnvVarName = "REPO_PASSWORD" val userEnvVarName = "REPO_USER" val password = "1234" val user = "user" val envVars = Map( userEnvVarName -> user, passwordEnvVarName -> password ) val configEnv = Map("SCALA_CLI_CONFIG" -> configFile.toString) val keys = List("repositories.credentials", "publish.credentials") TestInputs.empty.fromRoot { root => for (key <- keys) { os.proc( TestUtil.cli, "--power", "config", key, "s1.oss.sonatype.org", s"env:$userEnvVarName", s"env:$passwordEnvVarName", "--password-value" ) .call(cwd = root, env = configEnv ++ envVars) val credsFromConfig = os.proc(TestUtil.cli, "--power", "config", key) .call(cwd = root, env = configEnv) .out.trim() expect(credsFromConfig.contains(password)) expect(credsFromConfig.contains(user)) } } } for ( (entryType, key, valuesPlural, invalidValue) <- Seq( ("boolean", "power", Seq("true", "false", "true"), "true."), ("string", "publish.user.name", Seq("abc", "def", "xyz"), ""), ("password", "httpProxy.password", Seq("value:pass1", "value:pass2", "value:pass3"), "pass") ) ) { test(s"print a meaningful error when multiple values are passed for a $entryType key: $key") { val configFile = os.rel / "config" / "config.json" val env = Map("SCALA_CLI_CONFIG" -> configFile.toString) TestInputs.empty.fromRoot { root => val res = os.proc(TestUtil.cli, "--power", "config", key, valuesPlural) .call(cwd = root, env = env, stderr = os.Pipe, check = false) expect(res.exitCode == 1) expect(res.err.trim().contains(s"expected a single $entryType value")) } } if (entryType != "string") test(s"print a meaningful error when an invalid value is passed for a $entryType key: $key") { val configFile = os.rel / "config" / "config.json" val env = Map("SCALA_CLI_CONFIG" -> configFile.toString) TestInputs.empty.fromRoot { root => val res = os.proc(TestUtil.cli, "--power", "config", key, invalidValue) .call(cwd = root, env = env, stderr = os.Pipe, check = false) expect(res.exitCode == 1) expect(res.err.trim().contains("Malformed")) expect(res.err.trim().contains(invalidValue)) } } } test("change value for key") { val configFile = os.rel / "config" / "config.json" val configEnv = Map("SCALA_CLI_CONFIG" -> configFile.toString) val (props, props2, props3) = ("props=test", "props2=test2", "props3=test3") val key = "java.properties" TestInputs.empty.fromRoot { root => // set some values first time os.proc(TestUtil.cli, "--power", "config", key, props, props2).call( cwd = root, env = configEnv ) // override some values should throw error without force flag val res = os.proc(TestUtil.cli, "--power", "config", key, props, props2, props3).call( cwd = root, env = configEnv, check = false, mergeErrIntoOut = true ) expect(res.exitCode == 1) expect(res.out.trim().contains("pass -f or --force")) os.proc(TestUtil.cli, "--power", "config", key, props, props2, props3, "-f").call( cwd = root, env = configEnv, check = false ) val propertiesFromConfig = os.proc(TestUtil.cli, "--power", "config", key) .call(cwd = root, env = configEnv) .out.trim() expect(propertiesFromConfig.contains(props)) expect(propertiesFromConfig.contains(props2)) expect(propertiesFromConfig.contains(props3)) } } for { offlineSetting <- Seq(true, false) prefillCache <- if (offlineSetting) Seq(true, false) else Seq(false) caption = s"offline mode: $offlineSetting, " + (offlineSetting -> prefillCache match { case (true, true) => "build should succeed when cache was pre-filled" case (true, false) => "build should fail when cache is empty" case _ => "dependencies should be downloaded as normal" }) } test(caption) { TestInputs( os.rel / "simple.sc" -> "println(dotty.tools.dotc.config.Properties.versionNumberString)" ) .fromRoot { root => val configFile = os.rel / "config" / "config.json" val localRepoPath = root / "local-repo" val envs = Map( "COURSIER_CACHE" -> localRepoPath.toString, "SCALA_CLI_CONFIG" -> configFile.toString ) os.proc(TestUtil.cli, "bloop", "exit", "--power").call(cwd = root) os.proc(TestUtil.cli, "config", "offline", offlineSetting.toString) .call(cwd = root, env = envs) if (prefillCache) for { artifactName <- Seq( "scala3-compiler_3", "scala3-staging_3", "scala3-tasty-inspector_3", "scala3-sbt-bridge" ) artifact = s"org.scala-lang:$artifactName:${Constants.scala3Next}" } os.proc(TestUtil.cs, "fetch", "--cache", localRepoPath, artifact).call(cwd = root) val buildExpectedToSucceed = !offlineSetting || prefillCache val r = os.proc(TestUtil.cli, "run", "simple.sc", "--with-compiler") .call(cwd = root, env = envs, check = buildExpectedToSucceed) if (buildExpectedToSucceed) expect(r.out.trim() == Constants.scala3Next) else expect(r.exitCode == 1) os.proc(TestUtil.cli, "config", "offline", "--unset") .call(cwd = root, env = envs) os.proc(TestUtil.cli, "bloop", "exit", "--power").call(cwd = root) } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/CoursierScalaInstallationTestHelper.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect import java.nio.charset.Charset import scala.jdk.CollectionConverters.IteratorHasAsScala import scala.util.Properties trait CoursierScalaInstallationTestHelper { def withScalaRunnerWrapper( root: os.Path, localBin: os.Path, scalaVersion: String, localCache: Option[os.Path] = None, shouldCleanUp: Boolean = true )(f: os.Path => Unit): Unit = { val localCacheArgs = localCache.fold(Seq.empty[String])(c => Seq("--cache", c.toString)) os.proc( TestUtil.cs, "install", localCacheArgs, "--install-dir", localBin, s"scala:$scalaVersion" ).call(cwd = root) val (launchScalaPath: os.Path, underlyingScriptPath: os.Path) = if (Properties.isWin) { val batchWrapperScript: os.Path = localBin / "scala.bat" val charset = Charset.defaultCharset().toString val batchWrapperContent = new String(os.read.bytes(batchWrapperScript), charset) val setCommandLine = batchWrapperContent .lines() .iterator() .asScala .toList .find(_.startsWith("SET CMDLINE=")) .getOrElse("") val scriptPathRegex = """SET CMDLINE="(.*\\bin\\scala\.bat)" %CMD_LINE_ARGS%""".r val batchScript = setCommandLine match { case scriptPathRegex(extractedPath) => extractedPath } val batchScriptPath = os.Path(batchScript) val oldContent = os.read(batchScriptPath) val newContent = oldContent.replace( "call %SCALA_CLI_CMD_WIN%", s"""set "SCALA_CLI_CMD_WIN=${TestUtil.cliPath}" |call %SCALA_CLI_CMD_WIN%""".stripMargin ) expect(newContent != oldContent) os.write.over(batchScriptPath, newContent) batchWrapperScript -> batchScriptPath } else { val scalaBinary: os.Path = localBin / "scala" val fileBytes = os.read.bytes(scalaBinary) val shebang = new String(fileBytes.takeWhile(_ != '\n'), "UTF-8") val binaryData = fileBytes.drop(shebang.length + 1) val execLine = new String(binaryData.takeWhile(_ != '\n'), "UTF-8") val scriptPathRegex = """exec "([^"]+/bin/scala).*"""".r val scalaScript = execLine match { case scriptPathRegex(extractedPath) => extractedPath } val scalaScriptPath = os.Path(scalaScript) val lineToChange = "eval \"${SCALA_CLI_CMD_BASH[@]}\" \\" val changedLine = s"""eval \"${TestUtil.cli.mkString(s"\" \\${System.lineSeparator()}")}\" \\""" val newContent = os.read(scalaScriptPath).replace(lineToChange, changedLine) os.write.over(scalaScriptPath, newContent) scalaBinary -> scalaScriptPath } val wrapperVersion = os.proc(launchScalaPath, "version", "--cli-version") .call(cwd = root).out.trim() val cliVersion = os.proc(TestUtil.cli, "version", "--cli-version") .call(cwd = root).out.trim() expect(wrapperVersion == cliVersion) f(launchScalaPath) if (shouldCleanUp) { // clean up cs local binaries val csPrebuiltBinaryDir = os.Path(underlyingScriptPath.toString().substring( 0, underlyingScriptPath.toString().indexOf(scalaVersion) + scalaVersion.length )) System.err.println(s"Cleaning up, trying to remove $csPrebuiltBinaryDir") try { os.remove.all(csPrebuiltBinaryDir) System.err.println(s"Cleanup complete. Removed $csPrebuiltBinaryDir") } catch { case ex: java.nio.file.FileSystemException => System.err.println(s"Failed to remove $csPrebuiltBinaryDir: $ex") } } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/DefaultFileTests.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect class DefaultFileTests extends ScalaCliSuite { override def group: ScalaCliSuite.TestGroup = ScalaCliSuite.TestGroup.First test("Print .gitignore") { val res = os.proc(TestUtil.cli, "--power", "default-file", ".gitignore") .call() val output = res.out.text() expect(output.linesIterator.toVector.contains("/.scala-build/")) } test("Write .gitignore") { TestInputs.empty.fromRoot { root => os.proc(TestUtil.cli, "--power", "default-file", ".gitignore", "--write") .call(cwd = root, stdout = os.Inherit) val dest = root / ".gitignore" expect(os.isFile(dest)) val content = os.read(dest) expect(content.linesIterator.toVector.contains("/.scala-build/")) } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/DefaultTests.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect class DefaultTests extends WithWarmUpScalaCliSuite with LegacyScalaRunnerTestDefinitions { override def warmUpExtraTestOptions: Seq[String] = TestUtil.extraOptions test("running scala-cli with no args should default to repl") { TestInputs.empty.fromRoot { root => val res = os.proc(TestUtil.cli, "--repl-dry-run").call(cwd = root, mergeErrIntoOut = true) expect(res.out.lines().lastOption.contains(replDryRunOutput)) } } test("running scala-cli with no args should not accept run-only options") { TestInputs.empty.fromRoot { root => val runSpecificOption = "--list-main-classes" val res = os.proc(TestUtil.cli, runSpecificOption).call( cwd = root, mergeErrIntoOut = true, check = false ) expect(res.exitCode == 1) expect(res.out.lines().endsWith(unrecognizedArgMessage(runSpecificOption))) } } test("running scala-cli with args should not accept repl-only options") { TestInputs(os.rel / "Hello.sc" -> """println("Hello")""").fromRoot { root => val replSpecificOption = "--ammonite" val res = os.proc(TestUtil.cli, "--power", ".", replSpecificOption).call( cwd = root, mergeErrIntoOut = true, check = false ) expect(res.exitCode == 1) expect(res.out.lines().endsWith(unrecognizedArgMessage(replSpecificOption))) } } test("default to the run sub-command when a scala snippet is passed with --execute-scala") { TestInputs.empty.fromRoot { root => val msg = "Hello world" val quotation = TestUtil.argQuotationMark val res = os.proc( TestUtil.cli, "--execute-scala", s"@main def main() = println($quotation$msg$quotation)", TestUtil.extraOptions ) .call(cwd = root) expect(res.out.trim() == msg) } } test("default to the run sub-command when a java snippet is passed with --execute-java") { TestInputs.empty.fromRoot { root => val msg = "Hello world" val quotation = TestUtil.argQuotationMark val res = os.proc( TestUtil.cli, "--execute-java", s"public class Main { public static void main(String[] args) { System.out.println($quotation$msg$quotation); } }", TestUtil.extraOptions ) .call(cwd = root) expect(res.out.trim() == msg) } } test("default to the run sub-command when a md snippet is passed with --execute-markdown") { TestInputs.empty.fromRoot { root => val msg = "Hello world" val quotation = TestUtil.argQuotationMark val res = os.proc( TestUtil.cli, "--power", "--execute-markdown", s"""# A Markdown snippet |With some scala code |```scala |println($quotation$msg$quotation) |```""".stripMargin, TestUtil.extraOptions ) .call(cwd = root) expect(res.out.trim() == msg) } } test("default to the run sub-command if -classpath and --main-class are passed") { val expectedOutput = "Hello" val mainClassName = "Main" TestInputs( os.rel / s"$mainClassName.scala" -> s"""object $mainClassName extends App { println("$expectedOutput") }""" ).fromRoot { (root: os.Path) => val compilationOutputDir = os.rel / "compilationOutput" // first, precompile to an explicitly specified output directory with -d os.proc( TestUtil.cli, ".", "-d", compilationOutputDir ).call(cwd = root) // next, run while relying on the pre-compiled class instead of passing inputs val runRes = os.proc( TestUtil.cli, "--main-class", mainClassName, "-classpath", (os.rel / compilationOutputDir).toString ).call(cwd = root) expect(runRes.out.trim() == expectedOutput) } } test("default to the repl sub-command if -classpath is passed, but --main-class isn't") { val expectedOutput = "Hello" val mainClassName = "Main" TestInputs( os.rel / s"$mainClassName.scala" -> s"""object $mainClassName extends App { println("$expectedOutput") }""" ).fromRoot { (root: os.Path) => val compilationOutputDir = os.rel / "compilationOutput" // first, precompile to an explicitly specified output directory with -d os.proc( TestUtil.cli, ".", "-d", compilationOutputDir ).call(cwd = root) // next, run the repl while relying on the pre-compiled classes val runRes = os.proc( TestUtil.cli, "--repl-dry-run", "-classpath", (os.rel / compilationOutputDir).toString ).call(cwd = root, mergeErrIntoOut = true) expect(runRes.out.lines().lastOption.contains(replDryRunOutput)) } } test("ensure --help-native works with the default command") { TestInputs.empty.fromRoot { root => val res = os.proc(TestUtil.cli, "--help-native").call(cwd = root) expect(res.out.trim().contains("Scala Native options:")) } } test("ensure --help-js works with the default command") { TestInputs.empty.fromRoot { root => val res = os.proc(TestUtil.cli, "--help-js").call(cwd = root) expect(res.out.trim().contains("Scala.js options:")) } } protected def unrecognizedArgMessage(argName: String): Vector[String] = s""" |Unrecognized argument: $argName | |To list all available options, run | ${Console.BOLD}${TestUtil.detectCliPath} --help${Console.RESET} |""".stripMargin.trim.linesIterator.toVector protected lazy val replDryRunOutput = "Dry run, not running REPL." } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/DependencyUpdateTests.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect import coursier.version.Version class DependencyUpdateTests extends ScalaCliSuite { test("dependency update test") { val fileName = "Hello.scala" val message = "Hello World" val fileContent = s"""|//> using dep com.lihaoyi::os-lib:0.7.8 |//> using dep com.lihaoyi::utest:0.7.10 | |object Hello extends App { | println("$message") |}""".stripMargin val inputs = TestInputs(os.rel / fileName -> fileContent) inputs.fromRoot { root => // update dependencies val p = os.proc(TestUtil.cli, "--power", "dependency-update", "--all", fileName) .call( cwd = root, stdin = os.Inherit, mergeErrIntoOut = true ) expect(p.out.trim().contains("Updated dependency")) expect( // check if dependency update command modify file os.read(root / fileName) != fileContent ) // after updating dependencies app should run val out = os.proc(TestUtil.cli, fileName).call(cwd = root).out.trim() expect(out == message) } } test("update toolkit dependency") { val toolkitVersion = "0.1.3" val testInputs = TestInputs( os.rel / "Foo.scala" -> s"""//> using toolkit $toolkitVersion | |object Hello extends App { | println("Hello") |} |""".stripMargin ) testInputs.fromRoot { root => // update toolkit os.proc(TestUtil.cli, "--power", "dependency-update", "--all", ".") .call(cwd = root) val toolkitDirective = "//> using toolkit (.*)".r val updatedToolkitVersionOpt = { val regexMatch = toolkitDirective.findFirstMatchIn(os.read(root / "Foo.scala")) regexMatch.map(_.group(1)) } expect(updatedToolkitVersionOpt.nonEmpty) val updatedToolkitVersion = updatedToolkitVersionOpt.get expect(Version(updatedToolkitVersion) > Version(toolkitVersion)) } } test("update typelevel toolkit dependency") { val toolkitVersion = "0.0.1" val testInputs = TestInputs( os.rel / "Foo.scala" -> s"""//> using toolkit typelevel:$toolkitVersion | |import cats.effect.* | |object Hello extends IOApp.Simple { | def run = IO.println("Hello") |} |""".stripMargin ) testInputs.fromRoot { root => // update toolkit os.proc(TestUtil.cli, "--power", "dependency-update", "--all", ".") .call(cwd = root) val toolkitDirective = "//> using toolkit typelevel:(.*)".r val updatedToolkitVersionOpt = { val regexMatch = toolkitDirective.findFirstMatchIn(os.read(root / "Foo.scala")) regexMatch.map(_.group(1)) } expect(updatedToolkitVersionOpt.nonEmpty) val updatedToolkitVersion = updatedToolkitVersionOpt.get expect(Version(updatedToolkitVersion) > Version(toolkitVersion)) } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/DirectoriesTests.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect import scala.util.Properties class DirectoriesTests extends ScalaCliSuite { test("running directories with args should fail") { TestInputs(os.rel / "s.sc" -> """println("Hello")""").fromRoot { root => val r = os.proc(TestUtil.cli, "--power", "directories", "s.sc") .call(cwd = root, stderr = os.Pipe, check = false) expect(r.exitCode == 1) expect(r.err.trim() == "The directories command doesn't accept arguments.") } } if (Properties.isMac) test("running directories on Mac with no args should give valid results") { TestInputs.empty.fromRoot { root => val r = os.proc(TestUtil.cli, "--power", "directories").call(cwd = root) val cachesPath = os.home / "Library" / "Caches" / "ScalaCli" val appSupportPath = os.home / "Library" / "Application Support" / "ScalaCli" val expectedOutput = s"""Local repository: ${cachesPath / "local-repo"} |Completions: ${appSupportPath / "completions"} |Virtual projects: ${cachesPath / "virtual-projects"} |BSP sockets: ${cachesPath / "bsp-sockets"} |Bloop daemon directory: ${cachesPath / "bloop" / "daemon"} |Secrets directory: ${appSupportPath / "secrets"}""".stripMargin expect(r.out.trim() == expectedOutput) } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/DocTestDefinitions.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect import org.jsoup.* import scala.jdk.CollectionConverters.* abstract class DocTestDefinitions extends ScalaCliSuite with TestScalaVersionArgs { this: TestScalaVersion => protected lazy val extraOptions: Seq[String] = scalaVersionArgs ++ TestUtil.extraOptions for { useTestScope <- Seq(true, false) scopeOpts = if (useTestScope) Seq("--test", "--power") else Nil scopeDirective = if (useTestScope) "//> using target.scope test" else "" scopeDescription = scopeOpts.headOption.getOrElse("main") } test(s"generate static scala doc ($scopeDescription)") { val dest = os.rel / "doc-static" val inputs = TestInputs( os.rel / "lib" / "Messages.scala" -> """package lib | |object Messages { | def msg = "Hello" |} |""".stripMargin, os.rel / "simple.sc" -> s"""$scopeDirective |val msg = lib.Messages.msg |println(msg) |""".stripMargin ) inputs.fromRoot { root => os.proc(TestUtil.cli, "doc", extraOptions, ".", "-o", dest, scopeOpts).call( cwd = root, stdin = os.Inherit, stdout = os.Inherit ) val expectedDestDocPath = root / dest expect(os.isDir(expectedDestDocPath)) val expectedEntries = if (actualScalaVersion.startsWith("2.")) Seq( "index.html", "lib/Messages$.html", "simple$.html" ) else if ( actualScalaVersion.coursierVersion >= "3.5.0".coursierVersion || (actualScalaVersion.coursierVersion >= "3.3.4".coursierVersion && actualScalaVersion.coursierVersion < "3.4.0".coursierVersion) || actualScalaVersion.startsWith("3.3.4") || actualScalaVersion.startsWith("3.5") ) Seq( "index.html", "inkuire-db.json", "$lessempty$greater$/simple$_.html", "lib/Messages$.html" ) else Seq( "index.html", "inkuire-db.json", "_empty_/simple$_.html", "lib/Messages$.html" ) val entries = os.walk(root / dest).filter(!os.isDir(_)).map { path => path.relativeTo(expectedDestDocPath).toString() -> os.read(path) }.toMap expect(expectedEntries.forall(e => entries.contains(e))) val documentableNameElement = Jsoup.parse(entries("index.html")).select(".documentableName").asScala documentableNameElement.filter(_.text().contains("lib")).foreach { element => expect(!element.attr("href").startsWith("http")) } } } if actualScalaVersion.startsWith("3") then test("doc with compileOnly.dep") { TestInputs( os.rel / "project.scala" -> s"""//> using compileOnly.dep org.springframework.boot:spring-boot:3.5.6 |//> using test.dep org.springframework.boot:spring-boot:3.5.6 |""".stripMargin, os.rel / "RootLoggerConfigurer.scala" -> s"""import org.springframework.beans.factory.annotation.Autowired |import scala.compiletime.uninitialized | |class RootLoggerConfigurer: | @Autowired var sentryClient: String = uninitialized |""".stripMargin ).fromRoot(root => os.proc(TestUtil.cli, "doc", ".", extraOptions).call(cwd = root)) } if actualScalaVersion.startsWith("3") then for { javaVersion <- if isScala38OrNewer then Constants.allJavaVersions.filter(_ >= Constants.scala38MinJavaVersion) else Constants.allJavaVersions } test(s"doc generates correct external mapping URLs for JVM $javaVersion") { TestUtil.retryOnCi() { val dest = os.rel / "doc-out" val inputs = TestInputs( os.rel / "Lib.scala" -> """package mylib | |/** A wrapper around [[java.util.HashMap]] and [[scala.Option]]. */ |class Lib: | /** Returns a [[java.util.HashMap]]. */ | def getMap: java.util.HashMap[String, String] = new java.util.HashMap() | /** Returns a [[scala.Option]]. */ | def getOpt: Option[String] = Some("hello") |""".stripMargin ) inputs.fromRoot { root => os.proc( TestUtil.cli, "doc", extraOptions, ".", "-o", dest, "--jvm", javaVersion.toString ).call(cwd = root, stdin = os.Inherit, stdout = os.Inherit) val docDir = root / dest expect(os.isDir(docDir)) val htmlContent = os.walk(docDir) .filter(_.last.endsWith(".html")) .map(os.read(_)) .mkString val expectedJavadocFragment = if javaVersion >= 11 then s"docs.oracle.com/en/java/javase/$javaVersion/docs/api/java.base/" else s"docs.oracle.com/javase/$javaVersion/docs/api/" expect(htmlContent.contains(expectedJavadocFragment)) if javaVersion < 11 then expect(!htmlContent.contains("java.base/")) expect(htmlContent.contains(s"scala-lang.org/api/$actualScalaVersion/")) } } } test(s"doc --cross with multiple Scala versions produces doc output per cross") { val crossScalaVersions = Seq(actualScalaVersion, Constants.scala213, Constants.scala212) val dest = os.rel / "doc-cross" TestInputs( os.rel / "project.scala" -> s"//> using scala ${crossScalaVersions.mkString(" ")}", os.rel / "Lib.scala" -> """package mylib | |/** A sample class. */ |class Lib { | def value: Int = 42 |} |""".stripMargin ).fromRoot { root => os.proc( TestUtil.cli, "doc", "--cross", "--power", extraOptions, ".", "-o", dest ).call(cwd = root, stdin = os.Inherit, stdout = os.Inherit) val baseDocPath = root / dest expect(os.isDir(baseDocPath)) crossScalaVersions.foreach { version => val subDir = baseDocPath / version expect(os.isDir(subDir)) expect(os.list(subDir).exists(_.last.endsWith(".html"))) } } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/DocTests212.scala ================================================ package scala.cli.integration class DocTests212 extends DocTestDefinitions with Test212 ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/DocTests213.scala ================================================ package scala.cli.integration class DocTests213 extends DocTestDefinitions with Test213 ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/DocTests3Lts.scala ================================================ package scala.cli.integration class DocTests3Lts extends DocTestDefinitions with Test3Lts ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/DocTests3NextRc.scala ================================================ package scala.cli.integration class DocTests3NextRc extends DocTestDefinitions with Test3NextRc ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/DocTestsDefault.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect class DocTestsDefault extends DocTestDefinitions with TestDefault { test("javadoc") { val inputs = TestInputs( os.rel / "Foo.java" -> """//> using dep org.graalvm.nativeimage:svm:22.0.0.2 | |import com.oracle.svm.core.annotate.TargetClass; |import org.graalvm.nativeimage.Platform; |import org.graalvm.nativeimage.Platforms; | |/** | * Foo class | */ |@TargetClass(className = "something") |@Platforms({Platform.LINUX.class, Platform.DARWIN.class}) |public class Foo { | /** | * Gets the value | * | * @return the value | */ | public int getValue() { | return 2; | } |} |""".stripMargin ) inputs.fromRoot { root => val dest = root / "doc" os.proc(TestUtil.cli, "doc", extraOptions, ".", "-o", dest).call( cwd = root, stdin = os.Inherit, stdout = os.Inherit ) expect(os.isDir(dest)) val expectedEntries = Seq( "index.html", "overview-tree.html", "Foo.html" ) val entries = os.walk(dest).map(_.relativeTo(dest)).map(_.toString).toSet expect(expectedEntries.forall(entries)) } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/ExportCommonTestDefinitions.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect import java.nio.charset.Charset import scala.util.Properties trait ExportCommonTestDefinitions { this: ScalaCliSuite & TestScalaVersionArgs => protected lazy val extraOptions: Seq[String] = scalaVersionArgs ++ TestUtil.extraOptions ++ Seq("--suppress-experimental-warning") protected def commonTestDescriptionSuffix: String = "" protected def runExportTests: Boolean = Properties.isMac protected def exportCommand(args: String*): os.proc protected def defaultExportCommandArgs: Seq[String] = Nil protected def buildToolCommand(root: os.Path, mainClass: Option[String], args: String*): os.proc protected def runMainArgs(mainClass: Option[String]): Seq[String] protected def runTestsArgs(mainClass: Option[String]): Seq[String] protected val prepareTestInputs: TestInputs => TestInputs = identity protected val outputDir: os.RelPath = os.rel / "output-project" protected def simpleTest( inputs: TestInputs, mainClass: Option[String], extraExportArgs: Seq[String] = Nil ): Unit = prepareTestInputs(inputs).fromRoot { root => val exportArgs = "." +: extraExportArgs exportCommand(exportArgs*).call(cwd = root, stdout = os.Inherit) val res = buildToolCommand(root, mainClass, runMainArgs(mainClass)*).call(cwd = root / outputDir) val output = res.out.text(Charset.defaultCharset()) expect(output.contains("Hello from exported Scala CLI project")) } protected def jvmTest( mainArgs: Seq[String], testArgs: Seq[String], extraExportArgs: Seq[String] = Nil, mainClassName: String ): Unit = prepareTestInputs(ExportTestProjects.jvmTest(actualScalaVersion, mainClassName)).fromRoot { root => val exportArgs = "." +: extraExportArgs exportCommand(exportArgs*).call(cwd = root, stdout = os.Inherit) // main val res = buildToolCommand(root, Some(mainClassName), mainArgs*).call(cwd = root / outputDir) val output = res.out.text(Charset.defaultCharset()) expect(output.contains("Hello from " + actualScalaVersion)) // resource expect(output.contains("resource:1,2")) // test val testRes = buildToolCommand(root, Some(mainClassName), testArgs*).call(cwd = root / outputDir) val testOutput = testRes.out.text(Charset.defaultCharset()) expect( testOutput.contains("1 succeeded") || testOutput.contains("BUILD SUCCESS") ) // maven returns 'BUILD SUCCESS' } protected def scalaVersionTest( scalaVersion: String, mainClass: String, extraExportArgs: Seq[String] = Nil ): Unit = prepareTestInputs(ExportTestProjects.scalaVersionTest(scalaVersion, mainClass)).fromRoot { root => exportCommand("." +: extraExportArgs*).call(cwd = root, stdout = os.Inherit) val res = buildToolCommand(root, Some(mainClass), runMainArgs(Some(mainClass))*) .call(cwd = root / outputDir) val output = res.out.text(Charset.defaultCharset()) expect(output.contains("Hello")) } def extraSourceFromDirectiveWithExtraDependency( mainClass: String, extraExportArgs: Seq[String], inputs: String* ): Unit = prepareTestInputs( ExportTestProjects.extraSourceFromDirectiveWithExtraDependency(actualScalaVersion, mainClass) ).fromRoot { root => exportCommand(extraExportArgs ++ inputs*).call(cwd = root, stdout = os.Inherit) val res = buildToolCommand(root, Some(mainClass), runMainArgs(Some(mainClass))*) .call(cwd = root / outputDir) val output = res.out.trim(Charset.defaultCharset()) expect(output.contains(root.toString)) } def justTestScope(mainClass: String, extraExportArgs: Seq[String] = Nil): Unit = { val expectedMessage = "exporting just the test scope actually works!" prepareTestInputs(ExportTestProjects.justTestScope(mainClass, expectedMessage)) .fromRoot { root => exportCommand("." +: extraExportArgs*).call(cwd = root) val testRes = buildToolCommand(root, Some(mainClass), runTestsArgs(Some(mainClass))*) .call(cwd = root / outputDir) val testOutput = testRes.out.text() expect(testOutput.contains(expectedMessage)) } } protected val scalaVersionsInDir: Seq[String] = Seq("2.12", "2.13", "2", "3", "3.lts") if (runExportTests) { test(s"JVM$commonTestDescriptionSuffix") { TestUtil.retryOnCi() { jvmTest( mainArgs = runMainArgs(Some("Main")), testArgs = runTestsArgs(Some("Main")), mainClassName = "Main", extraExportArgs = defaultExportCommandArgs ) } } test(s"extra source from a directive introducing a dependency$commonTestDescriptionSuffix") { TestUtil.retryOnCi() { extraSourceFromDirectiveWithExtraDependency( mainClass = "Main", extraExportArgs = defaultExportCommandArgs, inputs = "Main.scala" ) } } test( s"extra source passed both via directive and from command line$commonTestDescriptionSuffix" ) { TestUtil.retryOnCi() { extraSourceFromDirectiveWithExtraDependency( mainClass = "Main", extraExportArgs = defaultExportCommandArgs, inputs = "." ) } } scalaVersionsInDir.foreach { scalaV => test( s"check export for project with scala version in directive as $scalaV$commonTestDescriptionSuffix" ) { TestUtil.retryOnCi() { scalaVersionTest( scalaVersion = scalaV, mainClass = "Main", extraExportArgs = defaultExportCommandArgs ) } } } test(s"just test scope$commonTestDescriptionSuffix") { // Keeping the test name ends with Test to support maven convention TestUtil.retryOnCi() { justTestScope(mainClass = "MyTest", extraExportArgs = defaultExportCommandArgs) } } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/ExportJsonTestDefinitions.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect abstract class ExportJsonTestDefinitions extends ScalaCliSuite with TestScalaVersionArgs { this: TestScalaVersion => private def readJson(path: os.ReadablePath): String = readJson(os.read(path)) private def readJson(json: String): String = json .replaceAll("\\s", "") .replaceAll( "ivy:file:[^\"]*/local-repo/[^\"]*", "ivy:file:.../local-repo/..." ) .replaceAll( "ivy:file:[^\"]*\\.ivy2/local[^\"]*", "ivy:file:.../.ivy2/local/" ) .replaceAll( "\"scalaCliVersion\":(\"[^\"]*\")", "\"scalaCliVersion\":\"1.1.1-SNAPSHOT\"" ) private def withEscapedBackslashes(s: os.Path): String = s.toString.replaceAll("\\\\", "\\\\\\\\") test("export json") { val inputs = TestInputs( os.rel / "Main.scala" -> """//> using dep com.lihaoyi::os-lib:0.7.8 | |object Main { | def main(args: Array[String]): Unit = | println("Hello") |} |""".stripMargin ) inputs.fromRoot { root => TestUtil.initializeGit(root, "v1.1.2") val exportJsonProc = // Test --power placed after subcommand name os.proc(TestUtil.cli, "export", "--power", "--json", ".", "--jvm", "temurin:11") .call(cwd = root) val jsonContents = readJson(exportJsonProc.out.text()) val expectedJsonContents = s"""{ |"projectVersion":"1.1.2", |"scalaVersion":"${Constants.scala3Next}", |"platform":"JVM", |"jvmVersion":"temurin:11", |"scopes": { | "main": { | "sources": ["${withEscapedBackslashes(root / "Main.scala")}"], | "dependencies": [ | { | "groupId":"com.lihaoyi", | "artifactId": { | "name":"os-lib", | "fullName": "os-lib_3" | }, | "version":"0.7.8" | } | ], | "resolvers": [ | "https://repo1.maven.org/maven2", | "ivy:file:.../local-repo/...", | "ivy:file:.../.ivy2/local/" | ] | } |} |,"scalaCliVersion":"1.1.1-SNAPSHOT" |} |""".replaceAll("\\s|\\|", "") expect(jsonContents == expectedJsonContents) } } test("export json with test scope") { val inputs = TestInputs( os.rel / "Main.scala" -> """//> using dep com.lihaoyi::os-lib:0.7.8 |//> using option -Xasync |//> using plugin org.wartremover:::wartremover:3.0.9 |//> using scala 3.2.2 | |object Main { | def main(args: Array[String]): Unit = | println("Hello") |} |""".stripMargin, os.rel / "unit.test.scala" -> """//> using repository sonatype:snapshots |//> using resourceDir ./resources |//> using jar TEST.jar |""".stripMargin ) inputs.fromRoot { root => val exportJsonProc = os.proc(TestUtil.cli, "--power", "export", "--json", ".", "--native") .call(cwd = root) val jsonContents = readJson(exportJsonProc.out.text()) val expectedJsonContents = s"""{ |"scalaVersion":"3.2.2", |"platform":"Native", |"scalaNativeVersion":"${Constants.scalaNativeVersion}", |"scopes": { | "main": { | "sources": ["${withEscapedBackslashes(root / "Main.scala")}"], | "scalacOptions":["-Xasync"], | "scalaCompilerPlugins": [ | { | "groupId": "org.wartremover", | "artifactId": { | "name": "wartremover", | "fullName": "wartremover_3.2.2" | }, | "version": "3.0.9" | } | ], | "dependencies": [ | { | "groupId":"com.lihaoyi", | "artifactId": { | "name":"os-lib", | "fullName": "os-lib_3" | }, | "version":"0.7.8" | } | ], | "resolvers": [ | "https://repo1.maven.org/maven2", | "ivy:file:.../local-repo/...", | "ivy:file:.../.ivy2/local/" | ] | }, | "test": { | "sources":["${withEscapedBackslashes(root / "unit.test.scala")}"], | "scalacOptions":["-Xasync"], | "scalaCompilerPlugins": [ | { | "groupId": "org.wartremover", | "artifactId": { | "name": "wartremover", | "fullName": "wartremover_3.2.2" | }, | "version": "3.0.9" | } | ], | "dependencies": [ | { | "groupId": "com.lihaoyi", | "artifactId": { | "name":"os-lib", | "fullName": "os-lib_3" | }, | "version": "0.7.8" | } | ], | "resolvers": [ | "https://oss.sonatype.org/content/repositories/snapshots", | "https://repo1.maven.org/maven2", | "ivy:file:.../local-repo/...", | "ivy:file:.../.ivy2/local/" | ], | "resourceDirs":["${withEscapedBackslashes(root / "resources")}"], | "customJarsDecls":["${withEscapedBackslashes(root / "TEST.jar")}"] | } |} |,"scalaCliVersion":"1.1.1-SNAPSHOT" |} |""".replaceAll("\\s|\\|", "") expect(jsonContents == expectedJsonContents) } } test("export json with js") { val inputs = TestInputs( os.rel / "Main.scala" -> """//> using scala 3.1.3 |//> using platform scala-js |//> using lib com.lihaoyi::os-lib:0.7.8 |//> using option -Xasync |//> using plugin org.wartremover:::wartremover:3.0.9 | |object Main { | def main(args: Array[String]): Unit = | println("Hello") |} |""".stripMargin ) inputs.fromRoot { root => val exportJsonProc = os.proc( TestUtil.cli, "--power", "export", "--json", "--output", "json_dir", ".", "--js-es-version", "es2015" ) .call(cwd = root) expect(exportJsonProc.out.text().isEmpty) val fileContents = readJson(root / "json_dir" / "export.json") val expectedFileContents = s"""{ |"scalaVersion": "3.1.3", |"platform": "JS", |"scalaJsVersion": "${Constants.scalaJsVersion}", |"jsEsVersion":"es2015", |"scopes": { | "main": { | "sources": ["${withEscapedBackslashes(root / "Main.scala")}"], | "scalacOptions": ["-Xasync"], | "scalaCompilerPlugins": [ | { | "groupId": "org.wartremover", | "artifactId": { | "name": "wartremover", | "fullName": "wartremover_3.1.3" | }, | "version": "3.0.9" | } | ], | "dependencies": [ | { | "groupId": "com.lihaoyi", | "artifactId": { | "name": "os-lib", | "fullName": "os-lib_3" | }, | "version": "0.7.8" | } | ], | "resolvers": [ | "https://repo1.maven.org/maven2", | "ivy:file:.../local-repo/...", | "ivy:file:.../.ivy2/local/" | ] | } |}, |"scalaCliVersion":"1.1.1-SNAPSHOT" |} |""".replaceAll("\\s|\\|", "") expect(fileContents == expectedFileContents) val exportToExistingProc = os.proc( TestUtil.cli, "--power", "export", "--json", "--output", "json_dir", ".", "--js-es-version", "es2015" ) .call(cwd = root, check = false, mergeErrIntoOut = true) expect(exportToExistingProc.exitCode != 0) val jsonDirPath = root / "json_dir" expect(exportToExistingProc.out.text().contains(s"Error: $jsonDirPath already exists.")) } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/ExportJsonTestsDefault.scala ================================================ package scala.cli.integration class ExportJsonTestsDefault extends ExportJsonTestDefinitions with TestDefault ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/ExportMavenTest3NextRc.scala ================================================ package scala.cli.integration class ExportMavenTest3NextRc extends ExportMavenTestDefinitions with Test3NextRc with MavenScala {} ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/ExportMavenTestDefinitions.scala ================================================ package scala.cli.integration abstract class ExportMavenTestDefinitions extends ScalaCliSuite with TestScalaVersionArgs with ExportCommonTestDefinitions with MavenTestHelper { this: TestScalaVersion & MavenLanguageMode => override def exportCommand(args: String*): os.proc = os.proc( TestUtil.cli, "--power", "export", extraOptions, "--mvn", "-o", outputDir.toString, args ) override def buildToolCommand(root: os.Path, mainClass: Option[String], args: String*): os.proc = mavenCommand(args*) override def runMainArgs(mainClass: Option[String]): Seq[String] = { require(mainClass.nonEmpty, "Main class or Test class is mandatory to build in maven") if (language == JAVA) Seq("exec:java", s"-Dexec.mainClass=${mainClass.get}") else Seq("scala:run", s"-DmainClass=${mainClass.get}") } override def runTestsArgs(mainClass: Option[String]): Seq[String] = if (language == JAVA) Seq("test") else Seq("test") } sealed trait Language case object JAVA extends Language case object SCALA extends Language sealed trait MavenLanguageMode { def language: Language } trait MavenJava extends MavenLanguageMode { final override def language: Language = JAVA } trait MavenScala extends MavenLanguageMode { final override def language: Language = SCALA } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/ExportMavenTestJava.scala ================================================ package scala.cli.integration import scala.util.Properties class ExportMavenTestJava extends ExportMavenTestDefinitions with Test3Lts with MavenJava { // disable running scala tests in java maven export override def runExportTests: Boolean = false if (!Properties.isWin) test("pure java") { simpleTest( ExportTestProjects.pureJavaTest("ScalaCliJavaTest"), mainClass = Some("ScalaCliJavaTest") ) } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/ExportMavenTests212.scala ================================================ package scala.cli.integration class ExportMavenTests212 extends ExportMavenTestDefinitions with Test212 with MavenScala ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/ExportMavenTests213.scala ================================================ package scala.cli.integration class ExportMavenTests213 extends ExportMavenTestDefinitions with Test213 with MavenScala ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/ExportMavenTests3Lts.scala ================================================ package scala.cli.integration class ExportMavenTests3Lts extends ExportMavenTestDefinitions with Test3Lts with MavenScala ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/ExportMill012Tests212.scala ================================================ package scala.cli.integration class ExportMill012Tests212 extends ExportMillTestDefinitions with Test212 with TestMill012 ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/ExportMill012Tests213.scala ================================================ package scala.cli.integration class ExportMill012Tests213 extends ExportMillTestDefinitions with Test213 with TestMill012 { if runExportTests then { test(s"scalac options$commonTestDescriptionSuffix") { simpleTest( inputs = ExportTestProjects.scalacOptionsScala2Test(actualScalaVersion), mainClass = None, extraExportArgs = defaultExportCommandArgs ) } test(s"pure java$commonTestDescriptionSuffix") { simpleTest( inputs = ExportTestProjects.pureJavaTest("ScalaCliJavaTest"), mainClass = None, extraExportArgs = defaultExportCommandArgs ) } test(s"custom JAR$commonTestDescriptionSuffix") { simpleTest( inputs = ExportTestProjects.customJarTest(actualScalaVersion), mainClass = None, extraExportArgs = defaultExportCommandArgs ) } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/ExportMill012Tests3Lts.scala ================================================ package scala.cli.integration class ExportMill012Tests3Lts extends ExportMillTestDefinitions with Test3Lts with TestMill012 ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/ExportMill012Tests3NextRc.scala ================================================ package scala.cli.integration class ExportMill012Tests3NextRc extends ExportMillTestDefinitions with Test3NextRc with TestMill012 ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/ExportMill012TestsDefault.scala ================================================ package scala.cli.integration class ExportMill012TestsDefault extends ExportMillTestDefinitions with TestDefault with TestMill012 ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/ExportMill1Tests212.scala ================================================ package scala.cli.integration class ExportMill1Tests212 extends ExportMillTestDefinitions with Test212 with TestMill1 ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/ExportMill1Tests213.scala ================================================ package scala.cli.integration class ExportMill1Tests213 extends ExportMillTestDefinitions with Test213 with TestMill1 { if runExportTests then { test(s"scalac options$commonTestDescriptionSuffix") { simpleTest( inputs = ExportTestProjects.scalacOptionsScala2Test(actualScalaVersion), mainClass = None, extraExportArgs = defaultExportCommandArgs ) } test(s"pure java$commonTestDescriptionSuffix") { simpleTest( inputs = ExportTestProjects.pureJavaTest("ScalaCliJavaTest"), mainClass = None, extraExportArgs = defaultExportCommandArgs ) } test(s"custom JAR$commonTestDescriptionSuffix") { simpleTest( inputs = ExportTestProjects.customJarTest(actualScalaVersion), mainClass = None, extraExportArgs = defaultExportCommandArgs ) } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/ExportMill1Tests3Lts.scala ================================================ package scala.cli.integration class ExportMill1Tests3Lts extends ExportMillTestDefinitions with Test3Lts with TestMill1 ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/ExportMill1Tests3NextRc.scala ================================================ package scala.cli.integration class ExportMill1Tests3NextRc extends ExportMillTestDefinitions with Test3NextRc with TestMill1 ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/ExportMill1TestsDefault.scala ================================================ package scala.cli.integration class ExportMill1TestsDefault extends ExportMillTestDefinitions with TestDefault with TestMill1 ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/ExportMillTestDefinitions.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect import os.RelPath import java.nio.charset.Charset abstract class ExportMillTestDefinitions extends ScalaCliSuite with TestScalaVersionArgs with ExportCommonTestDefinitions with ExportScalaOrientedBuildToolsTestDefinitions with MillTestHelper { this: TestScalaVersion & TestMillVersion => override val prepareTestInputs: TestInputs => TestInputs = _.withMillJvmOpts override val outputDir: RelPath = millOutputDir override def exportCommand(args: String*): os.proc = os.proc( TestUtil.cli, "--power", "export", extraOptions, "--mill", "-o", outputDir.toString, args ) override def buildToolCommand(root: os.Path, mainClass: Option[String], args: String*): os.proc = millCommand(root, args*) override def runMainArgs(mainClass: Option[String]): Seq[String] = Seq(s"$millDefaultProjectName.run") override def runTestsArgs(mainClass: Option[String]): Seq[String] = Seq(s"$millDefaultProjectName.test") override def commonTestDescriptionSuffix = s" (Mill $millVersion & Scala $actualScalaVersion)" override protected def defaultExportCommandArgs: Seq[String] = Seq("--mill-version", millVersion) def jvmTestScalacOptions(className: String, exportArgs: Seq[String]): Unit = ExportTestProjects.jvmTest(actualScalaVersion, className).withMillJvmOpts.fromRoot { root => exportCommand(exportArgs :+ "."*).call(cwd = root, stdout = os.Inherit) val res = buildToolCommand( root, Some(className), "--disable-ticker", "show", s"$millDefaultProjectName.scalacOptions" ) .call(cwd = root / outputDir) val output = res.out.text(Charset.defaultCharset()) expect(output.filterNot(_.isWhitespace) == "[\"-deprecation\"]") } def jvmTestCompilerPlugin(mainClass: String, exportArgs: Seq[String]): Unit = { val message = "Hello" ExportTestProjects.jvmTestWithCompilerPlugin( scalaVersion = actualScalaVersion, mainClassName = mainClass, message = message ) .withMillJvmOpts.fromRoot { root => exportCommand(exportArgs :+ "."*).call(cwd = root, stdout = os.Inherit) locally { val millDepsCommand = if millVersion.startsWith("1.") then "scalacPluginMvnDeps" else "scalacPluginIvyDeps" val res = buildToolCommand( root, Some(mainClass), "show", s"$millDefaultProjectName.$millDepsCommand" ) .call(cwd = root / outputDir) val output = res.out.text(Charset.defaultCharset()) expect(output.contains("hearth-cross-quotes")) } locally { val res = buildToolCommand(root, Some(mainClass), s"$millDefaultProjectName.run") .call(cwd = root / outputDir) val output = res.out.text(Charset.defaultCharset()) expect(output.contains(message)) } } } if runExportTests then { test(s"JVM custom project name$commonTestDescriptionSuffix") { TestUtil.retryOnCi() { val customProjectName = "newproject" jvmTest( mainArgs = Seq(s"$customProjectName.run"), testArgs = Seq(s"$customProjectName.test"), extraExportArgs = Seq("-p", customProjectName) ++ defaultExportCommandArgs, mainClassName = "Hello" ) } } test(s"JVM scalac options$commonTestDescriptionSuffix") { TestUtil.retryOnCi() { jvmTestScalacOptions(className = "Hello", exportArgs = defaultExportCommandArgs) } } if !actualScalaVersion.startsWith("2.12") then test(s"JVM with a compiler plugin$commonTestDescriptionSuffix") { TestUtil.retryOnCi() { jvmTestCompilerPlugin(mainClass = "Hello", exportArgs = defaultExportCommandArgs) } } } } sealed trait TestMillVersion: def millVersion: String trait TestMill012 extends TestMillVersion: self: ExportMillTestDefinitions => override def millVersion: String = Constants.mill012Version trait TestMill1 extends TestMillVersion: self: ExportMillTestDefinitions => override def millVersion: String = Constants.mill1Version ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/ExportSbtTestDefinitions.scala ================================================ package scala.cli.integration abstract class ExportSbtTestDefinitions extends ScalaCliSuite with TestScalaVersionArgs with ExportCommonTestDefinitions with ExportScalaOrientedBuildToolsTestDefinitions with SbtTestHelper { this: TestScalaVersion => override def exportCommand(args: String*): os.proc = os.proc( TestUtil.cli, "--power", "export", extraOptions, "--sbt", "-o", outputDir.toString, args ) override def buildToolCommand(root: os.Path, mainClass: Option[String], args: String*): os.proc = sbtCommand(args*) override def runMainArgs(mainClass: Option[String]): Seq[String] = Seq("run") override def runTestsArgs(mainClass: Option[String]): Seq[String] = Seq("test") test("Scala Native") { TestUtil.retryOnCi() { simpleTest(ExportTestProjects.nativeTest(actualScalaVersion), mainClass = None) } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/ExportSbtTests212.scala ================================================ package scala.cli.integration class ExportSbtTests212 extends ExportSbtTestDefinitions with Test212 ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/ExportSbtTests213.scala ================================================ package scala.cli.integration class ExportSbtTests213 extends ExportSbtTestDefinitions with Test213 { if (runExportTests) { test("scalac options") { simpleTest(ExportTestProjects.scalacOptionsScala2Test(actualScalaVersion), mainClass = None) } test("pure java") { simpleTest( ExportTestProjects.pureJavaTest("ScalaCliJavaTest"), mainClass = None, extraExportArgs = Seq("--sbt-setting=fork := true") ) } test("custom JAR") { simpleTest(ExportTestProjects.customJarTest(actualScalaVersion), mainClass = None) } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/ExportSbtTests3Lts.scala ================================================ package scala.cli.integration class ExportSbtTests3Lts extends ExportSbtTestDefinitions with Test3Lts ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/ExportSbtTests3NextRc.scala ================================================ package scala.cli.integration class ExportSbtTests3NextRc extends ExportSbtTestDefinitions with Test3NextRc ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/ExportSbtTestsDefault.scala ================================================ package scala.cli.integration class ExportSbtTestsDefault extends ExportSbtTestDefinitions with TestDefault ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/ExportScalaOrientedBuildToolsTestDefinitions.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect import java.nio.charset.Charset /** This is a trait that defined test definitions for scala-oriented build tools like sbt and mill. * The build tools like maven doesn't support some of the features like scalaJs, ScalaNative or * compile-only dependencies. */ trait ExportScalaOrientedBuildToolsTestDefinitions { this: ExportCommonTestDefinitions & ScalaCliSuite & TestScalaVersionArgs => def compileOnlyTest(mainClass: String, extraExportArgs: Seq[String] = Nil): Unit = { val userName = "John" prepareTestInputs( ExportTestProjects.compileOnlySource(actualScalaVersion, userName = userName) ).fromRoot { root => exportCommand("." +: extraExportArgs*).call(cwd = root, stdout = os.Inherit) val res = buildToolCommand(root, None, runMainArgs(Some(mainClass))*) .call(cwd = root / outputDir) val output = res.out.trim(Charset.defaultCharset()) expect(output.contains(userName)) expect(!output.contains("jsoniter-scala-macros")) } } def testZioTest(testClassName: String, extraExportArgs: Seq[String] = Nil): Unit = { val testInput = TestInputs( // todo: remove this hack after the PR https://github.com/VirtusLab/scala-cli/pull/3046 is merged os.rel / "Hello.scala" -> """object Hello extends App""", os.rel / "Zio.test.scala" -> s"""|//> using dep dev.zio::zio::1.0.8 |//> using dep dev.zio::zio-test-sbt::1.0.8 | |import zio._ |import zio.test._ |import zio.test.Assertion.equalTo | |object $testClassName extends DefaultRunnableSpec { | def spec = suite("associativity")( | testM("associativity") { | check(Gen.anyInt, Gen.anyInt, Gen.anyInt) { (x, y, z) => | assert((x + y) + z)(equalTo(x + (y + z))) | } | } | ) |} |""".stripMargin, os.rel / "input" / "input" -> """|1 |2""".stripMargin ) prepareTestInputs(testInput).fromRoot { root => val exportArgs = "." +: extraExportArgs val testArgsToPass = runTestsArgs(None) exportCommand(exportArgs*).call(cwd = root, stdout = os.Inherit) val testRes = buildToolCommand(root, None, testArgsToPass*).call(cwd = root / outputDir) val testOutput = testRes.out.text(Charset.defaultCharset()) expect(testOutput.contains("1 succeeded")) } } protected def logbackBugCase(mainClass: String, extraExportArgs: Seq[String] = Nil): Unit = prepareTestInputs(ExportTestProjects.logbackBugCase(actualScalaVersion)).fromRoot { root => exportCommand("." +: extraExportArgs*).call(cwd = root, stdout = os.Inherit) val res = buildToolCommand(root, Some(mainClass), runMainArgs(Some(mainClass))*) .call(cwd = root / outputDir) val output = res.out.text(Charset.defaultCharset()) expect(output.contains("Hello")) } if runExportTests then { test(s"compile-time only for jsoniter macros$commonTestDescriptionSuffix") { TestUtil.retryOnCi() { compileOnlyTest(mainClass = "main", extraExportArgs = defaultExportCommandArgs) } } test(s"Scala.js$commonTestDescriptionSuffix") { TestUtil.retryOnCi() { simpleTest( inputs = ExportTestProjects.jsTest(actualScalaVersion), mainClass = None, extraExportArgs = defaultExportCommandArgs ) } } test(s"zio test$commonTestDescriptionSuffix") { TestUtil.retryOnCi() { testZioTest(testClassName = "ZioSpec", extraExportArgs = defaultExportCommandArgs) } } test( s"Ensure test framework NPE is not thrown when depending on logback$commonTestDescriptionSuffix" ) { TestUtil.retryOnCi() { logbackBugCase(mainClass = "main", extraExportArgs = defaultExportCommandArgs) } } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/ExportTestProjects.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect import scala.cli.integration.Constants.munitVersion object ExportTestProjects { def jvmTest(scalaVersion: String, mainClassName: String): TestInputs = { val mainFile = if scalaVersion.startsWith("3.") then s"""//> using scala $scalaVersion |//> using resourceDir ./input |//> using dep org.scala-lang::scala3-compiler:$scalaVersion |//> using option -deprecation | |import scala.io.Source | |object $mainClassName { | def main(args: Array[String]): Unit = { | val message = "Hello from " + dotty.tools.dotc.config.Properties.simpleVersionString | println(message) | val inputs = Source.fromResource("input").getLines().map(_.toInt).toSeq | println(s"resource:$${inputs.mkString(",")}") | } |} |""".stripMargin else s"""//> using scala $scalaVersion |//> using resourceDir ./input |//> using option -deprecation |//> using plugins com.olegpy::better-monadic-for:0.3.1 | |import scala.io.Source | |object $mainClassName { | def main(args: Array[String]): Unit = { | val message = "Hello from " + scala.util.Properties.versionNumberString | println(message) | val inputs = Source.fromResource("input").getLines().map(_.toInt).toSeq | println(s"resource:$${inputs.mkString(",")}") | } |} |""".stripMargin TestInputs( os.rel / s"$mainClassName.scala" -> mainFile, os.rel / "Zio.test.scala" -> """|//> using dep dev.zio::zio::1.0.8 |//> using dep dev.zio::zio-test-sbt::1.0.8 | |import zio._ |import zio.test._ |import zio.test.Assertion.equalTo | |object HelloWorldSpec extends DefaultRunnableSpec { | def spec = suite("associativity")( | testM("associativity") { | check(Gen.anyInt, Gen.anyInt, Gen.anyInt) { (x, y, z) => | assert((x + y) + z)(equalTo(x + (y + z))) | } | } | ) |} |""".stripMargin, os.rel / "input" / "input" -> """|1 |2""".stripMargin ) } def jvmTestWithCompilerPlugin( scalaVersion: String, mainClassName: String, message: String ): TestInputs = TestInputs( os.rel / s"$mainClassName.scala" -> s"""//> using scala $scalaVersion |//> using option -deprecation |//> using plugin com.kubuszok::hearth-cross-quotes:0.2.0 | |object $mainClassName { | def main(args: Array[String]): Unit = { | println("$message") | } |} |""".stripMargin ) def jsTest(scalaVersion: String): TestInputs = { val testFile = if (scalaVersion.startsWith("3.")) s"""//> using scala $scalaVersion |//> using platform scala-js | |import scala.scalajs.js | |object Test: | def main(args: Array[String]): Unit = | val console = js.Dynamic.global.console | console.log("Hello from " + "exported Scala CLI project") |""".stripMargin else s"""//> using scala $scalaVersion |//> using platform scala-js | |import scala.scalajs.js | |object Test { | def main(args: Array[String]): Unit = { | val console = js.Dynamic.global.console | console.log("Hello from " + "exported Scala CLI project") | } |} |""".stripMargin TestInputs(os.rel / "Test.scala" -> testFile) } def nativeTest(scalaVersion: String): TestInputs = { val nl = "\\n" val testFile = if (scalaVersion.startsWith("3.")) s"""//> using scala $scalaVersion |//> using platform scala-native | |import scala.scalanative.libc._ |import scala.scalanative.unsafe._ | |object Test: | def main(args: Array[String]): Unit = | val message = "Hello from " + "exported Scala CLI project" + "$nl" | Zone { | val io = StdioHelpers(stdio) | io.printf(c"%s", toCString(message)) | } |""".stripMargin else s"""//> using scala $scalaVersion |//> using platform scala-native | |import scala.scalanative.libc._ |import scala.scalanative.unsafe._ | |object Test { | def main(args: Array[String]): Unit = { | val message = "Hello from " + "exported Scala CLI project" + "$nl" | Zone { implicit z => | val io = StdioHelpers(stdio) | io.printf(c"%s", toCString(message)) | } | } |} |""".stripMargin TestInputs(os.rel / "Test.scala" -> testFile) } def repositoryScala3Test(scalaVersion: String): TestInputs = { val testFile = s"""//> using scala $scalaVersion |//> using dep com.github.jupyter:jvm-repr:0.4.0 |//> using repository jitpack |import jupyter._ |object Test: | def main(args: Array[String]): Unit = | val message = "Hello from " + "exported Scala CLI project" | println(message) |""".stripMargin TestInputs(os.rel / "Test.scala" -> testFile) } def mainClassScala3Test(scalaVersion: String): TestInputs = { val testFile = s"""//> using scala $scalaVersion | |object Test: | def main(args: Array[String]): Unit = | val message = "Hello from " + "exported Scala CLI project" | println(message) |""".stripMargin val otherTestFile = s"""object Other: | def main(args: Array[String]): Unit = | val message = "Hello from " + "other file" | println(message) |""".stripMargin TestInputs( os.rel / "Test.scala" -> testFile, os.rel / "Other.scala" -> otherTestFile ) } def scalacOptionsScala2Test(scalaVersion: String): TestInputs = { val testFile = s"""//> using scala $scalaVersion |//> using dep org.scala-lang.modules::scala-async:0.10.0 |//> using dep org.scala-lang:scala-reflect:$scalaVersion |import scala.async.Async.{async, await} |import scala.concurrent.{Await, Future} |import scala.concurrent.duration.Duration |import scala.concurrent.ExecutionContext.Implicits.global | |object Test { | def main(args: Array[String]): Unit = { | val messageF = Future.successful( | "Hello from " + "exported Scala CLI project" | ) | val f = async { | val message = await(messageF) | println(message) | } | Await.result(f, Duration.Inf) | } |} |""".stripMargin TestInputs(os.rel / "Test.scala" -> testFile) } def pureJavaTest(mainClass: String): TestInputs = { val testFile = s"""public class $mainClass { | public static void main(String[] args) { | String className = "scala.concurrent.ExecutionContext"; | ClassLoader cl = Thread.currentThread().getContextClassLoader(); | boolean found = true; | try { | cl.loadClass(className); | } catch (ClassNotFoundException ex) { | found = false; | } | if (found) { | throw new RuntimeException("Didn't expect " + className + " to be in class path."); | } | System.out.println("Hello from " + "exported Scala CLI project"); | } |} |""".stripMargin TestInputs(os.rel / "ScalaCliJavaTest.java" -> testFile) } def testFrameworkTest(scalaVersion: String): TestInputs = { val testFile = s"""//> using scala $scalaVersion |//> using dep com.lihaoyi::utest:0.7.10 |//> using test-framework utest.runner.Framework | |import utest._ | |object MyTests extends TestSuite { | val tests = Tests { | test("foo") { | assert(2 + 2 == 4) | println("Hello from " + "exported Scala CLI project") | } | } |} |""".stripMargin TestInputs(os.rel / "MyTests.scala" -> testFile) } def customJarTest(scalaVersion: String): TestInputs = { val shapelessJar = { val res = os.proc( TestUtil.cs, "fetch", "--intransitive", "com.chuusai::shapeless:2.3.9", "--scala", scalaVersion ) .call() val path = res.out.trim() val path0 = os.Path(path, os.pwd) expect(os.isFile(path0)) path0 } val shapelessJarStr = "\"" + shapelessJar.toString.replace("\\", "\\\\") + "\"" val testFile = s"""//> using scala $scalaVersion |//> using jar $shapelessJarStr | |import shapeless._ | |object Test { | def main(args: Array[String]): Unit = { | val l = "exported Scala CLI project" :: 2 :: true :: HNil | val messageEnd: String = l.head | println("Hello from " + messageEnd) | } |} |""".stripMargin TestInputs(os.rel / "Test.scala" -> testFile) } def logbackBugCase(scalaVersion: String): TestInputs = TestInputs(os.rel / "script.sc" -> s"""//> using scala $scalaVersion |//> using dep ch.qos.logback:logback-classic:1.4.5 |println("Hello") |""".stripMargin) def extraSourceFromDirectiveWithExtraDependency( scalaVersion: String, mainClass: String ): TestInputs = TestInputs( os.rel / s"$mainClass.scala" -> s"""//> using scala $scalaVersion |//> using file Message.scala |object $mainClass extends App { | println(Message(value = os.pwd.toString).value) |} |""".stripMargin, os.rel / "Message.scala" -> s"""//> using dep com.lihaoyi::os-lib:0.9.1 |case class Message(value: String) |""".stripMargin ) def compileOnlySource(scalaVersion: String, userName: String): TestInputs = TestInputs( os.rel / "Hello.scala" -> s"""//> using scala $scalaVersion |//> using dep com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-core:2.23.2 |//> using compileOnly.dep com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros:2.23.2 | |import com.github.plokhotnyuk.jsoniter_scala.core._ |import com.github.plokhotnyuk.jsoniter_scala.macros._ | |object Hello extends App { | case class User(name: String, friends: Seq[String]) | implicit val codec: JsonValueCodec[User] = JsonCodecMaker.make | | val user = readFromString[User]("{\\"name\\":\\"$userName\\",\\"friends\\":[\\"Mark\\"]}") | System.out.println(user.name) | val classPath = System.getProperty("java.class.path").split(java.io.File.pathSeparator).iterator.toList | System.out.println(classPath) |} |""".stripMargin ) def scalaVersionTest(scalaVersion: String, mainClass: String): TestInputs = TestInputs( os.rel / "Hello.scala" -> s"""//> using scala $scalaVersion |object $mainClass extends App { | println("Hello") |} |""".stripMargin ) def justTestScope(testClass: String, msg: String): TestInputs = TestInputs( os.rel / "MyTests.test.scala" -> s"""//> using dep org.scalameta::munit::$munitVersion | |class $testClass extends munit.FunSuite { | test("foo") { | assert(2 + 2 == 4) | println("$msg") | } |} |""".stripMargin ) } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/FixBuiltInRulesTestDefinitions.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect import scala.util.Properties trait FixBuiltInRulesTestDefinitions { this: FixTestDefinitions => test("basic built-in rules") { val mainFileName = "Main.scala" val inputs = TestInputs( os.rel / mainFileName -> s"""//> using objectWrapper |//> using dep com.lihaoyi::os-lib:0.9.1 com.lihaoyi::upickle:3.1.2 | |package com.foo.main | |object Main extends App { | println(os.pwd) |} |""".stripMargin, os.rel / projectFileName -> s"""//> using deps com.lihaoyi::pprint:0.6.6 |""".stripMargin ) inputs.fromRoot { root => val fixOutput = os.proc( TestUtil.cli, "--power", "fix", ".", "-v", "-v", extraOptions, enableRulesOptions(enableScalafix = false) ) .call(cwd = root, mergeErrIntoOut = true).out.trim() assertNoDiff( filterDebugOutputs(fixOutput), """Running built-in rules... |Extracting directives from Main.scala |Extracting directives from project.scala |Writing project.scala |Removing directives from Main.scala |Built-in rules completed.""".stripMargin ) val projectFileContents = os.read(root / projectFileName) val mainFileContents = os.read(root / mainFileName) assertNoDiff( projectFileContents, """// Main |//> using objectWrapper |//> using dependency com.lihaoyi::os-lib:0.9.1 com.lihaoyi::pprint:0.6.6 com.lihaoyi::upickle:3.1.2 |""".stripMargin ) assertNoDiff( mainFileContents, """package com.foo.main | |object Main extends App { | println(os.pwd) |} |""".stripMargin ) val runProc = os.proc(TestUtil.cli, "--power", "compile", ".", extraOptions) .call(cwd = root, stderr = os.Pipe) expect(!runProc.err.trim().contains("Using directives detected in multiple files")) } } test("built-in rules for script with shebang") { val mainFileName = "main.sc" val inputs = TestInputs( os.rel / mainFileName -> s"""#!/usr/bin/env -S scala-cli shebang | |//> using objectWrapper |//> using dep com.lihaoyi::os-lib:0.9.1 com.lihaoyi::upickle:3.1.2 | |println(os.pwd) |""".stripMargin, os.rel / projectFileName -> s"""//> using deps com.lihaoyi::pprint:0.6.6 |""".stripMargin ) inputs.fromRoot { root => val fixOutput = os.proc( TestUtil.cli, "--power", "fix", ".", "-v", "-v", extraOptions, enableRulesOptions(enableScalafix = false) ) .call(cwd = root, mergeErrIntoOut = true).out.trim() assertNoDiff( filterDebugOutputs(fixOutput), """Running built-in rules... |Extracting directives from project.scala |Extracting directives from main.sc |Writing project.scala |Removing directives from main.sc |Built-in rules completed.""".stripMargin ) val projectFileContents = os.read(root / projectFileName) val mainFileContents = os.read(root / mainFileName) assertNoDiff( projectFileContents, """// Main |//> using objectWrapper |//> using dependency com.lihaoyi::os-lib:0.9.1 com.lihaoyi::pprint:0.6.6 com.lihaoyi::upickle:3.1.2 |""".stripMargin ) assertNoDiff( mainFileContents, """#!/usr/bin/env -S scala-cli shebang | |println(os.pwd) |""".stripMargin ) val runProc = os.proc(TestUtil.cli, "--power", "compile", ".", extraOptions) .call(cwd = root, stderr = os.Pipe) expect(!runProc.err.trim().contains("Using directives detected in multiple files")) } } test("built-in rules with test scope") { val mainSubPath = os.rel / "src" / "Main.scala" val testSubPath = os.rel / "test" / "MyTests.scala" val inputs = TestInputs( mainSubPath -> s"""//> using objectWrapper |//> using dep com.lihaoyi::os-lib:0.9.1 | |//> using test.dep org.typelevel::cats-core:2.9.0 | |package com.foo.main | |object Main extends App { | println(os.pwd) |} |""".stripMargin, testSubPath -> s"""//> using options -Xasync -Xfatal-warnings |//> using dep org.scalameta::munit::0.7.29 | |package com.foo.test.bar | |class MyTests extends munit.FunSuite { | test("bar") { | assert(2 + 2 == 4) | println("Hello from " + "tests") | } |} |""".stripMargin, os.rel / projectFileName -> s"""//> using deps com.lihaoyi::pprint:0.6.6 |""".stripMargin ) inputs.fromRoot { root => val fixOutput = os.proc( TestUtil.cli, "--power", "fix", ".", "-v", "-v", extraOptions, enableRulesOptions(enableScalafix = false) ) .call(cwd = root, mergeErrIntoOut = true).out.trim() assertNoDiff( filterDebugOutputs(fixOutput), """Running built-in rules... |Extracting directives from project.scala |Extracting directives from src/Main.scala |Extracting directives from test/MyTests.scala |Writing project.scala |Removing directives from src/Main.scala |Removing directives from test/MyTests.scala |Built-in rules completed.""".stripMargin ) val projectFileContents = os.read(root / projectFileName) val mainFileContents = os.read(root / mainSubPath) val testFileContents = os.read(root / testSubPath) assertNoDiff( projectFileContents, """// Main |//> using objectWrapper |//> using dependency com.lihaoyi::os-lib:0.9.1 com.lihaoyi::pprint:0.6.6 | |// Test |//> using test.options -Xasync -Xfatal-warnings |//> using test.dependency org.scalameta::munit::0.7.29 org.typelevel::cats-core:2.9.0 |""".stripMargin ) assertNoDiff( mainFileContents, """package com.foo.main | |object Main extends App { | println(os.pwd) |} |""".stripMargin ) assertNoDiff( testFileContents, """package com.foo.test.bar | |class MyTests extends munit.FunSuite { | test("bar") { | assert(2 + 2 == 4) | println("Hello from " + "tests") | } |} |""".stripMargin ) val runProc = os.proc(TestUtil.cli, "--power", "compile", ".", extraOptions) .call(cwd = root, stderr = os.Pipe) expect(!runProc.err.trim().contains("Using directives detected in multiple files")) } } test("built-in rules with complex inputs") { val mainSubPath = os.rel / "src" / "Main.scala" val testSubPath = os.rel / "test" / "MyTests.scala" val withUsedTargetSubPath = os.rel / "src" / "UsedTarget.scala" val withUsedTargetContents = s"""//> using target.scala 3.3.0 |//> using dep com.lihaoyi::upickle:3.1.2 |case class UsedTarget(x: Int) |""".stripMargin val withUnusedTargetSubPath = os.rel / "src" / "UnusedTarget.scala" val withUnusedTargetContents = s"""//> using target.scala 2.13 |//> using dep com.lihaoyi::upickle:3.1.2 |case class UnusedTarget(x: Int) |""".stripMargin val includedInputs = TestInputs( os.rel / "Included.scala" -> """//> using options -Werror | |case class Included(x: Int) |""".stripMargin ) includedInputs.fromRoot { includeRoot => val includePath = (includeRoot / "Included.scala").toString.replace("\\", "\\\\") val inputs = TestInputs( mainSubPath -> s"""//> using platforms jvm |//> using scala 3.3.0 |//> using jvm 17 |//> using objectWrapper |//> using dep com.lihaoyi::os-lib:0.9.1 |//> using file $includePath | |//> using test.dep org.typelevel::cats-core:2.9.0 | |package com.foo.main | |object Main extends App { | println(os.pwd) |} |""".stripMargin, withUsedTargetSubPath -> withUsedTargetContents, withUnusedTargetSubPath -> withUnusedTargetContents, testSubPath -> s"""//> using options -Xasync -Xfatal-warnings |//> using dep org.scalameta::munit::0.7.29 |//> using scala 3.2.2 | |package com.foo.test.bar | |class MyTests extends munit.FunSuite { | test("bar") { | assert(2 + 2 == 4) | println("Hello from " + "tests") | } |} |""".stripMargin, os.rel / projectFileName -> s"""//> using deps com.lihaoyi::pprint:0.6.6 | |//> using publish.ci.password env:PUBLISH_PASSWORD |//> using publish.ci.secretKey env:PUBLISH_SECRET_KEY |//> using publish.ci.secretKeyPassword env:PUBLISH_SECRET_KEY_PASSWORD |//> using publish.ci.user env:PUBLISH_USER |""".stripMargin ) inputs.fromRoot { root => val res = os.proc( TestUtil.cli, "--power", "fix", ".", "--script-snippet", "//> using toolkit default", "-v", "-v", extraOptions, enableRulesOptions(enableScalafix = false) ).call(cwd = root, stderr = os.Pipe) assertNoDiff( filterDebugOutputs(res.err.trim()), s"""Running built-in rules... |Extracting directives from project.scala |Extracting directives from src/Main.scala |Extracting directives from src/UsedTarget.scala |Extracting directives from ${includeRoot / "Included.scala"} |Extracting directives from snippet |Extracting directives from test/MyTests.scala |Writing project.scala |Removing directives from src/Main.scala |Removing directives from test/MyTests.scala | Keeping: | //> using scala 3.2.2 |Built-in rules completed.""".stripMargin ) val projectFileContents = os.read(root / projectFileName) val mainFileContents = os.read(root / mainSubPath) val testFileContents = os.read(root / testSubPath) val withUsedTargetContentsRead = os.read(root / withUsedTargetSubPath) val withUnusedTargetContentsRead = os.read(root / withUnusedTargetSubPath) assertNoDiff( projectFileContents, s"""// Main |//> using scala 3.3.0 |//> using platforms jvm |//> using jvm 17 |//> using options -Werror |//> using files $includePath |//> using objectWrapper |//> using toolkit default |//> using dependency com.lihaoyi::os-lib:0.9.1 com.lihaoyi::pprint:0.6.6 | |//> using publish.ci.password env:PUBLISH_PASSWORD |//> using publish.ci.secretKey env:PUBLISH_SECRET_KEY |//> using publish.ci.secretKeyPassword env:PUBLISH_SECRET_KEY_PASSWORD |//> using publish.ci.user env:PUBLISH_USER | |// Test |//> using test.options -Xasync -Xfatal-warnings |//> using test.dependency org.scalameta::munit::0.7.29 org.typelevel::cats-core:2.9.0 |""".stripMargin ) assertNoDiff( mainFileContents, """package com.foo.main | |object Main extends App { | println(os.pwd) |} |""".stripMargin ) // Directives with no 'test.' equivalent are retained assertNoDiff( testFileContents, """//> using scala 3.2.2 | |package com.foo.test.bar | |class MyTests extends munit.FunSuite { | test("bar") { | assert(2 + 2 == 4) | println("Hello from " + "tests") | } |} |""".stripMargin ) assertNoDiff(withUsedTargetContents, withUsedTargetContentsRead) assertNoDiff(withUnusedTargetContents, withUnusedTargetContentsRead) } assertNoDiff( os.read(includeRoot / "Included.scala"), """//> using options -Werror | |case class Included(x: Int) |""".stripMargin ) } } if (!Properties.isWin) // TODO: fix this test for Windows CI test("using directives with boolean values are handled correctly") { val expectedMessage = "Hello, world!" def maybeScalapyPrefix = if (actualScalaVersion.startsWith("2.13.")) "" else "import me.shadaj.scalapy.py" + System.lineSeparator() TestInputs( os.rel / "Messages.scala" -> s"""object Messages { | def hello: String = "$expectedMessage" |} |""".stripMargin, os.rel / "Main.scala" -> s"""//> using python true |$maybeScalapyPrefix |object Main extends App { | py.Dynamic.global.print(Messages.hello, flush = true) |} |""".stripMargin ).fromRoot { root => os.proc(TestUtil.cli, "--power", "fix", ".", extraOptions) .call(cwd = root, stderr = os.Pipe) val r = os.proc(TestUtil.cli, "--power", "run", ".", extraOptions) .call(cwd = root, stderr = os.Pipe) expect(r.out.trim() == expectedMessage) } } { val directive = "//> using dep com.lihaoyi::os-lib:0.11.3" for { (inputFileName, code) <- Seq( "raw.scala" -> s"""$directive |object Main extends App { | println(os.pwd) |} |""".stripMargin, "script.sc" -> s"""$directive |println(os.pwd) |""".stripMargin ) if !Properties.isWin // TODO: make this run on Windows CI testInputs = TestInputs(os.rel / inputFileName -> code) } test( s"dont extract directives into project.scala for a single-file project: $inputFileName" ) { testInputs.fromRoot { root => val fixResult = os.proc(TestUtil.cli, "--power", "fix", ".", extraOptions) .call(cwd = root, stderr = os.Pipe) expect(fixResult.err.trim().contains( "No need to migrate directives for a single source file project" )) expect(!os.exists(root / projectFileName)) expect(os.read(root / inputFileName) == code) val runResult = os.proc(TestUtil.cli, "run", ".", extraOptions) .call(cwd = root, stderr = os.Pipe) expect(runResult.out.trim() == root.toString) } } } if (!Properties.isWin) test("all test directives get extracted into project.scala") { val osLibDep = "com.lihaoyi::os-lib:0.11.5" val munitDep = "org.scalameta::munit:1.1.1" val pprintDep = "com.lihaoyi::pprint:0.9.3" val osLibDepDirective = s"//> using dependency $osLibDep" val osLibTestDepDirective = s"//> using test.dependency $osLibDep" val munitTestDepDirective = s"//> using test.dependency $munitDep" val pprintTestDepDirective = s"//> using test.dependency $pprintDep" val mainFilePath = os.rel / "Main.scala" val testFilePath = os.rel / "MyTests.test.scala" TestInputs( mainFilePath -> s"""$munitTestDepDirective |object Main extends App { | def hello: String = "Hello, world!" | println(hello) |} |""".stripMargin, testFilePath -> s"""$osLibDepDirective |$pprintTestDepDirective |import munit.FunSuite | |class MyTests extends FunSuite { | test("hello") { | pprint.pprintln(os.pwd) | assert(Main.hello == "Hello, world!") | } |} |""".stripMargin ).fromRoot { root => os.proc(TestUtil.cli, "--power", "fix", ".", extraOptions).call(cwd = root) val expectedProjectFileContents = s"""// Test |$osLibTestDepDirective |$pprintTestDepDirective |$munitTestDepDirective""".stripMargin val projectFileContents = os.read(root / projectFileName) expect(projectFileContents.trim() == expectedProjectFileContents) val mainFileContents = os.read(root / mainFilePath) expect(!mainFileContents.contains("//> using")) val testFileContents = os.read(root / testFilePath) expect(!testFileContents.contains("//> using")) os.proc(TestUtil.cli, "test", ".", extraOptions).call(cwd = root) } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/FixScalafixRulesTestDefinitions.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect import scala.util.Properties trait FixScalafixRulesTestDefinitions { this: FixTestDefinitions => protected val scalafixConfFileName: String = ".scalafix.conf" protected def scalafixUnusedRuleOption: String protected def noCrLf(input: String): String = input.replaceAll("\r\n", "\n") private val simpleInputsOriginalContent: String = """package foo | |final object Hello { | def main(args: Array[String]): Unit = { | println("Hello") | } |} |""".stripMargin private val simpleInputs: TestInputs = TestInputs( os.rel / scalafixConfFileName -> s"""|rules = [ | RedundantSyntax |] |""".stripMargin, os.rel / "Hello.scala" -> simpleInputsOriginalContent ) private val expectedContent: String = noCrLf { """package foo | |object Hello { | def main(args: Array[String]): Unit = { | println("Hello") | } |} |""".stripMargin } test("simple") { simpleInputs.fromRoot { root => os.proc(TestUtil.cli, "fix", ".", "--power", scalaVersionArgs).call(cwd = root) val updatedContent = noCrLf(os.read(root / "Hello.scala")) expect(updatedContent == expectedContent) } } test("with --check") { simpleInputs.fromRoot { root => val res = os.proc(TestUtil.cli, "fix", "--power", "--check", ".", scalaVersionArgs).call( cwd = root, check = false ) expect(res.exitCode != 0) val updatedContent = noCrLf(os.read(root / "Hello.scala")) expect(updatedContent == noCrLf(simpleInputsOriginalContent)) } } test("semantic rule") { val unusedValueInputsContent: String = s"""//> using options $scalafixUnusedRuleOption |package foo | |object Hello { | def main(args: Array[String]): Unit = { | val name = "John" | println("Hello") | } |} |""".stripMargin val semanticRuleInputs: TestInputs = TestInputs( os.rel / scalafixConfFileName -> s"""|rules = [ | RemoveUnused |] |""".stripMargin, os.rel / "Hello.scala" -> unusedValueInputsContent ) val expectedContent: String = noCrLf { s"""//> using options $scalafixUnusedRuleOption |package foo | |object Hello { | def main(args: Array[String]): Unit = { | | println("Hello") | } |} |""".stripMargin } semanticRuleInputs.fromRoot { root => os.proc(TestUtil.cli, "fix", "--power", ".", scalaVersionArgs).call(cwd = root) val updatedContent = noCrLf(os.read(root / "Hello.scala")) expect(updatedContent == expectedContent) } } test("--rules args") { val input = TestInputs( os.rel / scalafixConfFileName -> s"""|rules = [ | RemoveUnused, | RedundantSyntax |] |""".stripMargin, os.rel / "Hello.scala" -> s"""|//> using options $scalafixUnusedRuleOption |package hello | |object Hello { | def a = { | val x = 1 // keep unused - exec only RedundantSyntax | s"Foo" | } |} |""".stripMargin ) input.fromRoot { root => os.proc( TestUtil.cli, "fix", ".", "--scalafix-rules", "RedundantSyntax", "--power", scalaVersionArgs ).call(cwd = root) val updatedContent = noCrLf(os.read(root / "Hello.scala")) val expected = noCrLf { s"""|//> using options $scalafixUnusedRuleOption |package hello | |object Hello { | def a = { | val x = 1 // keep unused - exec only RedundantSyntax | "Foo" | } |} |""".stripMargin } expect(updatedContent == expected) } } test("--scalafix-arg arg") { val original: String = """|package foo | |final object Hello { // keep `final` beucase of parameter finalObject=false | s"Foo" |} |""".stripMargin val inputs: TestInputs = TestInputs( os.rel / scalafixConfFileName -> s"""|rules = [ | RedundantSyntax |] |""".stripMargin, os.rel / "Hello.scala" -> original ) val expectedContent: String = noCrLf { """|package foo | |final object Hello { // keep `final` beucase of parameter finalObject=false | "Foo" |} |""".stripMargin } inputs.fromRoot { root => os.proc( TestUtil.cli, "fix", ".", "--scalafix-arg=--settings.RedundantSyntax.finalObject=false", "--power", scalaVersionArgs ).call(cwd = root) val updatedContent = noCrLf(os.read(root / "Hello.scala")) expect(updatedContent == expectedContent) } } test("--scalafix-conf arg") { val original: String = """|package foo | |final object Hello { | s"Foo" |} |""".stripMargin val confFileName = "unusual-scalafix-filename" val inputs: TestInputs = TestInputs( os.rel / confFileName -> s"""|rules = [ | RedundantSyntax |] |""".stripMargin, os.rel / "Hello.scala" -> original ) val expectedContent: String = noCrLf { """|package foo | |object Hello { | "Foo" |} |""".stripMargin } inputs.fromRoot { root => os.proc( TestUtil.cli, "fix", ".", s"--scalafix-conf=$confFileName", "--power", scalaVersionArgs ).call(cwd = root) val updatedContent = noCrLf(os.read(root / "Hello.scala")) expect(updatedContent == expectedContent) } } test("external rule") { val directive = s"//> using scalafixDependency com.github.xuwei-k::scalafix-rules:0.5.1" val original: String = s"""|$directive | |object CollectHeadOptionTest { | def x1: Option[String] = List(1, 2, 3).collect { case n if n % 2 == 0 => n.toString }.headOption |} |""".stripMargin val inputs: TestInputs = TestInputs( os.rel / scalafixConfFileName -> s"""|rules = [ | CollectHeadOption |] |""".stripMargin, os.rel / "Hello.scala" -> original ) val expectedContent: String = noCrLf { s"""|$directive | |object CollectHeadOptionTest { | def x1: Option[String] = List(1, 2, 3).collectFirst{ case n if n % 2 == 0 => n.toString } |} |""".stripMargin } inputs.fromRoot { root => os.proc(TestUtil.cli, "fix", ".", "--power", scalaVersionArgs).call(cwd = root) val updatedContent = noCrLf(os.read(root / "Hello.scala")) expect(updatedContent == expectedContent) } } test("explicit-result-types") { val original: String = """|package foo | |object Hello { | def a(a: Int) = "asdasd" + a.toString |} |""".stripMargin val inputs: TestInputs = TestInputs( os.rel / scalafixConfFileName -> s"""|rules = [ | ExplicitResultTypes |] |ExplicitResultTypes.fetchScala3CompilerArtifactsOnVersionMismatch = true |""".stripMargin, os.rel / "Hello.scala" -> original ) val expectedContent: String = noCrLf { """|package foo | |object Hello { | def a(a: Int): String = "asdasd" + a.toString |} |""".stripMargin } inputs.fromRoot { root => os.proc(TestUtil.cli, "fix", ".", "--power", scalaVersionArgs).call(cwd = root) val updatedContent = noCrLf(os.read(root / "Hello.scala")) expect(updatedContent == expectedContent) } } for { (semanticDbOptions, expectedSuccess) <- Seq( Nil -> true, // .semanticdb files should be implicitly generated when Scalafix is run Seq("--semanticdb") -> true, Seq("--semanticdb=false") -> false ) semanticDbOptionsDescription = if (semanticDbOptions.nonEmpty) s" (${semanticDbOptions.mkString(" ")})" else "" verb = if (expectedSuccess) "run" else "fail" if !Properties.isWin || expectedSuccess } test( s"scalafix rules requiring SemanticDB $verb correctly with test scope sources$semanticDbOptionsDescription" ) { val compilerOptions = if (actualScalaVersion.startsWith("2.12")) Seq("-Ywarn-unused-import") else Seq("-Wunused:imports") TestInputs( os.rel / scalafixConfFileName -> """rules = [ | DisableSyntax, | LeakingImplicitClassVal, | NoValInForComprehension, | ExplicitResultTypes, | OrganizeImports |] |ExplicitResultTypes.fetchScala3CompilerArtifactsOnVersionMismatch = true |""".stripMargin, os.rel / projectFileName -> s"""//> using test.dep org.scalameta::munit::${Constants.munitVersion} |//> using options ${compilerOptions.mkString(" ")} |""".stripMargin, os.rel / "example.test.scala" -> """import munit.FunSuite | |class Munit extends FunSuite { | test("foo") { | assert(2 + 2 == 4) | println("Hello from Munit") | } |} |""".stripMargin ).fromRoot { root => val res = os.proc(TestUtil.cli, "fix", ".", "--power", semanticDbOptions, extraOptions) .call(cwd = root, check = false, stderr = os.Pipe) val successful = res.exitCode == 0 expect(successful == expectedSuccess) if (!expectedSuccess) expect( res.err.trim().contains("SemanticDB files' generation was explicitly set to false") ) } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/FixTestDefinitions.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect abstract class FixTestDefinitions extends ScalaCliSuite with TestScalaVersionArgs with FixBuiltInRulesTestDefinitions with FixScalafixRulesTestDefinitions { this: TestScalaVersion => val projectFileName = "project.scala" val extraOptions: Seq[String] = scalaVersionArgs ++ TestUtil.extraOptions ++ Seq("--suppress-experimental-feature-warning") def enableRulesOptions( enableScalafix: Boolean = true, enableBuiltIn: Boolean = true ): Seq[String] = Seq( s"--enable-scalafix=${enableScalafix.toString}", s"--enable-built-in-rules=${enableBuiltIn.toString}" ) test("built-in + scalafix rules") { val mainFileName = "Main.scala" val unusedValName = "unused" val directive1 = "//> using dep com.lihaoyi::os-lib:0.11.3" val directive2 = "//> using dep com.lihaoyi::pprint:0.9.0" val mergedDirective1And2 = "using dependency com.lihaoyi::os-lib:0.11.3 com.lihaoyi::pprint:0.9.0" val directive3 = if (actualScalaVersion.startsWith("2")) "//> using options -Xlint:unused" else "//> using options -Wunused:all" TestInputs( os.rel / "Foo.scala" -> s"""$directive1 |object Foo { | def hello: String = "hello" |} |""".stripMargin, os.rel / "Bar.scala" -> s"""$directive2 |object Bar { | def world: String = "world" |} |""".stripMargin, os.rel / mainFileName -> s"""$directive3 |object Main { | def main(args: Array[String]): Unit = { | val unused = "unused" | pprint.pprintln(Foo.hello + Bar.world) | pprint.pprintln(os.pwd) | } |} |""".stripMargin, os.rel / scalafixConfFileName -> """rules = [ | RemoveUnused |] |""".stripMargin ).fromRoot { root => os.proc(TestUtil.cli, "fix", ".", extraOptions, "--power").call(cwd = root) val projectFileContents = os.read(root / projectFileName) expect(projectFileContents.contains(mergedDirective1And2)) expect(projectFileContents.contains(directive3)) val mainFileContents = os.read(root / mainFileName) expect(!mainFileContents.contains(unusedValName)) os.proc(TestUtil.cli, "compile", ".", extraOptions).call(cwd = root) } } test("sbt file in directory does not break fix") { TestInputs( os.rel / "Main.scala" -> """object Main { | def main(args: Array[String]): Unit = println("Hello") |} |""".stripMargin, os.rel / "build.sbt" -> """name := "my-project"""", os.rel / scalafixConfFileName -> """rules = [ | RedundantSyntax |] |""".stripMargin ).fromRoot { root => os.proc( TestUtil.cli, "--power", "fix", ".", extraOptions ).call(cwd = root) } } def filterDebugOutputs(output: String): String = output .linesIterator .filterNot(_.trim().contains("repo dir")) .filterNot(_.trim().contains("local repo")) .filterNot(_.trim().contains("archive url")) .mkString(System.lineSeparator()) } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/FixTests212.scala ================================================ package scala.cli.integration class FixTests212 extends FixTestDefinitions with Test212 { override val scalafixUnusedRuleOption: String = "-Ywarn-unused" } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/FixTests213.scala ================================================ package scala.cli.integration class FixTests213 extends FixTestDefinitions with Test213 { override val scalafixUnusedRuleOption: String = "-Wunused" } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/FixTests3Lts.scala ================================================ package scala.cli.integration class FixTests3Lts extends FixTestDefinitions with Test3Lts { override val scalafixUnusedRuleOption: String = "-Wunused:all" } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/FixTests3NextRc.scala ================================================ package scala.cli.integration class FixTests3NextRc extends FixTestDefinitions with Test3NextRc { override val scalafixUnusedRuleOption: String = "-Wunused:all" } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/FixTestsDefault.scala ================================================ package scala.cli.integration class FixTestsDefault extends FixTestDefinitions with TestDefault { override val scalafixUnusedRuleOption: String = "-Wunused:all" } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/FmtTests.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect import scala.cli.integration.TestUtil.removeAnsiColors class FmtTests extends ScalaCliSuite { override def group: ScalaCliSuite.TestGroup = ScalaCliSuite.TestGroup.First val confFileName = ".scalafmt.conf" val emptyInputs: TestInputs = TestInputs(os.rel / ".placeholder" -> "") val simpleInputsUnformattedContent: String = """package foo | | object Foo extends java.lang.Object { | def get() = 2 | } |""".stripMargin val simpleInputs: TestInputs = TestInputs( os.rel / confFileName -> s"""|version = "${Constants.defaultScalafmtVersion}" |runner.dialect = scala213 |""".stripMargin, os.rel / "Foo.scala" -> simpleInputsUnformattedContent ) val expectedSimpleInputsFormattedContent: String = noCrLf { """package foo | |object Foo extends java.lang.Object { | def get() = 2 |} |""".stripMargin } val simpleInputsWithFilter: TestInputs = TestInputs( os.rel / confFileName -> s"""|version = "${Constants.defaultScalafmtVersion}" |runner.dialect = scala213 |project.excludePaths = [ "glob:**/should/not/format/**.scala" ] |""".stripMargin, os.rel / "Foo.scala" -> expectedSimpleInputsFormattedContent, os.rel / "scripts" / "SomeScript.sc" -> "println()\n", os.rel / "should" / "not" / "format" / "ShouldNotFormat.scala" -> simpleInputsUnformattedContent ) val simpleInputsWithDialectOnly: TestInputs = TestInputs( os.rel / confFileName -> "runner.dialect = scala213".stripMargin, os.rel / "Foo.scala" -> simpleInputsUnformattedContent ) val simpleInputsWithVersionOnly: TestInputs = TestInputs( os.rel / confFileName -> "version = \"3.5.5\"".stripMargin, os.rel / "Foo.scala" -> simpleInputsUnformattedContent ) val simpleInputsWithCustomConfLocation: TestInputs = TestInputs( os.rel / "custom.conf" -> s"""|version = "3.5.5" |runner.dialect = scala213 |""".stripMargin, os.rel / "Foo.scala" -> simpleInputsUnformattedContent ) private def noCrLf(input: String): String = input.replaceAll("\r\n", "\n") test("simple") { simpleInputs.fromRoot { root => os.proc(TestUtil.cli, "fmt", ".").call(cwd = root) val updatedContent = noCrLf(os.read(root / "Foo.scala")) expect(updatedContent == expectedSimpleInputsFormattedContent) } } test("no inputs") { simpleInputs.fromRoot { root => os.proc(TestUtil.cli, "fmt").call(cwd = root) val updatedContent = noCrLf(os.read(root / "Foo.scala")) expect(updatedContent == expectedSimpleInputsFormattedContent) } } test("with --check") { simpleInputs.fromRoot { root => val res = os.proc(TestUtil.cli, "fmt", "--check").call(cwd = root, check = false) expect(res.exitCode == 1) val out = res.out.text() val updatedContent = noCrLf(os.read(root / "Foo.scala")) expect(updatedContent == noCrLf(simpleInputsUnformattedContent)) expect(noCrLf(out) == "error: --test failed\n") } } test("filter correctly with --check") { simpleInputsWithFilter.fromRoot { root => val out = os.proc(TestUtil.cli, "fmt", ".", "--check").call(cwd = root).out.trim() expect(out == "All files are formatted with scalafmt :)") } } test("--scalafmt-help") { emptyInputs.fromRoot { root => val out1 = os.proc(TestUtil.cli, "fmt", "--scalafmt-help").call(cwd = root).out.trim() val out2 = os.proc(TestUtil.cli, "fmt", "-F", "--help").call(cwd = root).out.trim() expect(out1.nonEmpty) expect(out1 == out2) val outLines = out1.linesIterator.toSeq val outVersionLine = outLines.head expect(outVersionLine == s"scalafmt ${Constants.defaultScalafmtVersion}") val outUsageLine = outLines.drop(1).head expect(outUsageLine == "Usage: scalafmt [options] [...]") } } test("--save-scalafmt-conf") { simpleInputsWithDialectOnly.fromRoot { root => os.proc(TestUtil.cli, "fmt", ".", "--save-scalafmt-conf").call(cwd = root) val confLines = os.read.lines(root / confFileName) val versionInConf = confLines(0).stripPrefix("version = ") val updatedContent = noCrLf(os.read(root / "Foo.scala")) expect(versionInConf == s"\"${Constants.defaultScalafmtVersion}\"") expect(updatedContent == expectedSimpleInputsFormattedContent) } } test("--scalafmt-dialect") { simpleInputs.fromRoot { root => os.proc(TestUtil.cli, "fmt", ".", "--scalafmt-dialect", "scala3").call(cwd = root) val confLines = os.read.lines(root / Constants.workspaceDirName / confFileName) val dialectInConf = confLines(1).stripPrefix("runner.dialect = ").trim val updatedContent = noCrLf(os.read(root / "Foo.scala")) expect(dialectInConf == "scala3") expect(updatedContent == expectedSimpleInputsFormattedContent) } } test("--scalafmt-version") { simpleInputs.fromRoot { root => os.proc(TestUtil.cli, "fmt", ".", "--scalafmt-version", "3.5.5").call(cwd = root) val confLines = os.read.lines(root / Constants.workspaceDirName / confFileName) val versionInConf = confLines(0).stripPrefix("version = ").trim val updatedContent = noCrLf(os.read(root / "Foo.scala")) expect(versionInConf == "\"3.5.5\"") expect(updatedContent == expectedSimpleInputsFormattedContent) } } test("--scalafmt-conf") { simpleInputsWithCustomConfLocation.fromRoot { root => os.proc(TestUtil.cli, "fmt", ".", "--scalafmt-conf", "custom.conf").call(cwd = root) val confLines = os.read.lines(root / Constants.workspaceDirName / confFileName) val versionInConf = confLines(0).stripPrefix("version = ").trim val dialectInConf = confLines(1).stripPrefix("runner.dialect = ").trim val updatedContent = noCrLf(os.read(root / "Foo.scala")) expect(versionInConf == "\"3.5.5\"") expect(dialectInConf == "scala213") expect(updatedContent == expectedSimpleInputsFormattedContent) } } test("--scalafmt-conf-str") { simpleInputsWithVersionOnly.fromRoot { root => val confStr = s"""version = 3.5.7${System.lineSeparator}runner.dialect = scala213${System.lineSeparator}""" os.proc(TestUtil.cli, "fmt", ".", "--scalafmt-conf-str", s"$confStr").call(cwd = root) val confLines = os.read.lines(root / Constants.workspaceDirName / confFileName) val versionInConf = confLines(0).stripPrefix("version = ") val dialectInConf = confLines(1).stripPrefix("runner.dialect = ") val updatedContent = noCrLf(os.read(root / "Foo.scala")) expect(versionInConf == "\"3.5.7\"") expect(dialectInConf == "scala213") expect(updatedContent == expectedSimpleInputsFormattedContent) } } test("creating workspace conf file") { simpleInputsWithDialectOnly.fromRoot { root => val workspaceConfPath = root / Constants.workspaceDirName / confFileName expect(!os.exists(workspaceConfPath)) os.proc(TestUtil.cli, "fmt", ".").call(cwd = root) expect(os.exists(workspaceConfPath)) val updatedContent = noCrLf(os.read(root / "Foo.scala")) expect(updatedContent == expectedSimpleInputsFormattedContent) } } test("scalafmt conf without version") { simpleInputsWithDialectOnly.fromRoot { root => os.proc(TestUtil.cli, "fmt", ".").call(cwd = root) val confLines = os.read.lines(root / Constants.workspaceDirName / confFileName) val versionInConf = confLines(0).stripPrefix("version = ").trim val updatedContent = noCrLf(os.read(root / "Foo.scala")) expect(versionInConf == s"\"${Constants.defaultScalafmtVersion}\"") expect(updatedContent == expectedSimpleInputsFormattedContent) } } test("scalafmt conf without dialect") { simpleInputsWithVersionOnly.fromRoot { root => os.proc(TestUtil.cli, "fmt", ".").call(cwd = root) val confLines = os.read.lines(root / Constants.workspaceDirName / confFileName) val dialectInConf = confLines(1).stripPrefix("runner.dialect = ") val updatedContent = noCrLf(os.read(root / "Foo.scala")) expect(dialectInConf == "scala3") expect(updatedContent == expectedSimpleInputsFormattedContent) } } test("default values in help") { TestInputs.empty.fromRoot { root => val res = os.proc(TestUtil.cli, "fmt", "--help").call(cwd = root) val lines = removeAnsiColors(res.out.trim()).linesIterator.toVector val fmtVersionHelp = lines.find(_.contains("--fmt-version")).getOrElse("") expect(fmtVersionHelp.contains(s"(${Constants.defaultScalafmtVersion} by default)")) } } test("project.scala gets formatted correctly, as any other input") { val projectFileName = "project.scala" TestInputs( os.rel / projectFileName -> simpleInputsUnformattedContent, os.rel / confFileName -> s"""|version = "${Constants.defaultScalafmtVersion}" |runner.dialect = scala3 |""".stripMargin ) .fromRoot { root => os.proc(TestUtil.cli, "fmt", ".").call(cwd = root) val updatedContent = noCrLf(os.read(root / projectFileName)) expect(updatedContent == expectedSimpleInputsFormattedContent) } } val sbtUnformattedContent: String = """val message = "hello" |""".stripMargin val expectedSbtFormattedContent: String = noCrLf { """val message = "hello" |""".stripMargin } val sbtInputs: TestInputs = TestInputs( os.rel / confFileName -> s"""|version = "${Constants.defaultScalafmtVersion}" |runner.dialect = scala213 |""".stripMargin, os.rel / "build.sbt" -> sbtUnformattedContent ) test("sbt file is formatted when passed explicitly") { sbtInputs.fromRoot { root => os.proc(TestUtil.cli, "fmt", "build.sbt").call(cwd = root) val updatedContent = noCrLf(os.read(root / "build.sbt")) expect(updatedContent == expectedSbtFormattedContent) } } test("sbt file is formatted when directory is passed") { sbtInputs.fromRoot { root => os.proc(TestUtil.cli, "fmt", ".").call(cwd = root) val updatedContent = noCrLf(os.read(root / "build.sbt")) expect(updatedContent == expectedSbtFormattedContent) } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/GitHubTests.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect import com.github.plokhotnyuk.jsoniter_scala.core.* import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker import coursier.cache.ArchiveCache import coursier.util.Artifact import libsodiumjni.Sodium import libsodiumjni.internal.LoadLibrary import java.nio.charset.StandardCharsets import java.util.{Base64, Locale} import scala.util.Properties class GitHubTests extends ScalaCliSuite { override def group: ScalaCliSuite.TestGroup = ScalaCliSuite.TestGroup.First def createSecretTest(): Unit = { GitHubTests.initSodium() val keyId = "the-key-id" val keyPair = Sodium.keyPair() val value = "1234" TestInputs.empty.fromRoot { root => val pubKey = GitHubTests.PublicKey(keyId, Base64.getEncoder.encodeToString(keyPair.getPubKey)) os.write(root / "pub-key.json", writeToArray(pubKey)) val res = os.proc( TestUtil.cli, "--power", "github", "secret", "create", "--repo", "foo/foo", s"FOO=value:$value", "--dummy", "--print-request", "--pub-key", root / "pub-key.json" ) .call(cwd = root) val output = readFromArray(res.out.bytes)(GitHubTests.encryptedSecretCodec) expect(output.key_id == keyId) val decrypted = Sodium.sealOpen(output.encrypted, keyPair.getPubKey, keyPair.getSecKey) val decryptedValue = new String(decrypted, StandardCharsets.UTF_8) expect(decryptedValue == value) } } override def munitFlakyOK: Boolean = TestUtil.isCI def createSecret(): Unit = { try createSecretTest() catch { case e: UnsatisfiedLinkError if e.getMessage.contains("libsodium") => fail("libsodium, couldn't be loaded") } } // currently having issues loading libsodium from the static launcher // that launcher is mainly meant to be used on CIs or from docker, missing // that feature shouldn't be a big deal there if !TestUtil.isNativeStaticCli && !Properties.isMac && !TestUtil.isAarch64 then // TODO fix this for static launchers: https://github.com/VirtusLab/scala-cli/issues/4068 // TODO fix this for MacOS: https://github.com/VirtusLab/scala-cli/issues/4067 // TODO fix this for Linux arm64: https://github.com/VirtusLab/scala-cli/issues/4069 test("create secret")(TestUtil.retryOnCi()(createSecret())) } object GitHubTests { final case class PublicKey( key_id: String, key: String ) implicit val publicKeyCodec: JsonValueCodec[PublicKey] = JsonCodecMaker.make final case class EncryptedSecret( encrypted_value: String, key_id: String ) { def encrypted: Array[Byte] = Base64.getDecoder.decode(encrypted_value) } implicit val encryptedSecretCodec: JsonValueCodec[EncryptedSecret] = JsonCodecMaker.make private def condaLibsodiumVersion = Constants.condaLibsodiumVersion // Warning: somehow also in settings.sc in the build, and in FetchExternalBinary lazy val condaPlatform: String = { val mambaOs = if (Properties.isWin) "win" else if (Properties.isMac) "osx" else if (Properties.isLinux) "linux" else sys.error(s"Unsupported mamba OS: ${sys.props("os.name")}") val arch = sys.props("os.arch").toLowerCase(Locale.ROOT) val mambaArch = arch match { case "x86_64" | "amd64" => "64" case "arm64" | "aarch64" => "arm64" case "ppc64le" => "ppc64le" case _ => sys.error(s"Unsupported mamba architecture: $arch") } s"$mambaOs-$mambaArch" } private def archiveUrlAndPath() = { val suffix = condaPlatform match { case "linux-64" => "-h36c2ea0_1" case "linux-aarch64" => "-hb9de7d4_1" case "osx-64" => "-hbcb3906_1" case "osx-arm64" => "-h27ca646_1" case "win-64" => "-h62dcd97_1" case other => sys.error(s"Unrecognized conda platform $other") } val relPath = condaPlatform match { case "linux-64" => os.rel / "lib" / "libsodium.so" case "linux-aarch64" => os.rel / "lib" / "libsodium.so" case "osx-64" => os.rel / "lib" / "libsodium.dylib" case "osx-arm64" => os.rel / "lib" / "libsodium.dylib" case "win-64" => os.rel / "Library" / "bin" / "libsodium.dll" case other => sys.error(s"Unrecognized conda platform $other") } ( s"https://anaconda.org/conda-forge/libsodium/$condaLibsodiumVersion/download/$condaPlatform/libsodium-$condaLibsodiumVersion$suffix.tar.bz2", relPath ) } private def initSodium(): Unit = { val (url, relPath) = archiveUrlAndPath() val archiveCache = ArchiveCache() val dir = archiveCache.get(Artifact(url)).unsafeRun()(archiveCache.cache.ec) .fold(e => throw new Exception(e), os.Path(_, os.pwd)) val lib = dir / relPath System.load(lib.toString) LoadLibrary.initializeFromResources() Sodium.init() } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/HadoopTests.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect class HadoopTests extends munit.FunSuite { protected lazy val extraOptions: Seq[String] = TestUtil.extraOptions for { withTestScope <- Seq(true, false) scopeDescription = if (withTestScope) "test scope" else "main scope" inputPath = if (withTestScope) os.rel / "test" / "WordCount.java" else os.rel / "main" / "WordCount.java" directiveKey = if (withTestScope) "test.dep" else "dep" scopeOptions = if (withTestScope) Seq("--test") else Nil } test(s"simple map-reduce ($scopeDescription)") { TestUtil.retryOnCi() { val inputs = TestInputs( inputPath -> s"""//> using $directiveKey org.apache.hadoop:hadoop-client-api:3.3.3 | |// from https://hadoop.apache.org/docs/r3.3.3/hadoop-mapreduce-client/hadoop-mapreduce-client-core/MapReduceTutorial.html | |package foo; | |import java.io.IOException; |import java.util.StringTokenizer; | |import org.apache.hadoop.conf.Configuration; |import org.apache.hadoop.fs.Path; |import org.apache.hadoop.io.IntWritable; |import org.apache.hadoop.io.Text; |import org.apache.hadoop.mapreduce.Job; |import org.apache.hadoop.mapreduce.Mapper; |import org.apache.hadoop.mapreduce.Reducer; |import org.apache.hadoop.mapreduce.lib.input.FileInputFormat; |import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; | |public class WordCount { | | public static class TokenizerMapper | extends Mapper{ | | private final static IntWritable one = new IntWritable(1); | private Text word = new Text(); | | public void map(Object key, Text value, Context context | ) throws IOException, InterruptedException { | StringTokenizer itr = new StringTokenizer(value.toString()); | while (itr.hasMoreTokens()) { | word.set(itr.nextToken()); | context.write(word, one); | } | } | } | | public static class IntSumReducer | extends Reducer { | private IntWritable result = new IntWritable(); | | public void reduce(Text key, Iterable values, | Context context | ) throws IOException, InterruptedException { | int sum = 0; | for (IntWritable val : values) { | sum += val.get(); | } | result.set(sum); | context.write(key, result); | } | } | | public static void main(String[] args) throws Exception { | Configuration conf = new Configuration(); | Job job = Job.getInstance(conf, "word count"); | job.setJarByClass(WordCount.class); | job.setMapperClass(TokenizerMapper.class); | job.setCombinerClass(IntSumReducer.class); | job.setReducerClass(IntSumReducer.class); | job.setOutputKeyClass(Text.class); | job.setOutputValueClass(IntWritable.class); | FileInputFormat.addInputPath(job, new Path(args[0])); | FileOutputFormat.setOutputPath(job, new Path(args[1])); | System.exit(job.waitForCompletion(true) ? 0 : 1); | } |} |""".stripMargin ) inputs.fromRoot { root => val res = os.proc( TestUtil.cli, "--power", "run", TestUtil.extraOptions, ".", "--hadoop", "--command", "--scratch-dir", "tmp", scopeOptions, "--", "foo" ) .call(cwd = root) val command = res.out.lines() pprint.err.log(command) expect(command.take(2) == Seq("hadoop", "jar")) expect(command.takeRight(2) == Seq("foo.WordCount", "foo")) } } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/HelpTests.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect class HelpTests extends ScalaCliSuite { for (helpOptions <- HelpTests.variants) { lazy val help = os.proc(TestUtil.cli, helpOptions).call(check = false) lazy val helpOutput = help.out.trim() val helpOptionsString = helpOptions.mkString(" ") test(s"$helpOptionsString works correctly") { assert( help.exitCode == 0, clues(helpOptions, help.out.text(), help.err.text(), help.exitCode) ) expect(helpOutput.contains("Usage:")) } test(s"$helpOptionsString output command groups are ordered correctly") { val mainCommandsIndex = helpOutput.indexOf("Main commands:") val miscellaneousIndex = helpOutput.indexOf("Miscellaneous commands:") expect(mainCommandsIndex < miscellaneousIndex) expect(miscellaneousIndex < helpOutput.indexOf("Other commands:")) } test(s"$helpOptionsString output includes launcher options") { expect(helpOutput.contains("--power")) } test(s"$helpOptionsString output does not include legacy scala runner options") { expect(!helpOutput.contains("Legacy Scala runner options")) } test(s"$helpOptionsString output includes external help options") { expect(helpOutput.contains("--scalac-help")) expect(helpOutput.contains("--help-js")) expect(helpOutput.contains("--help-native")) expect(helpOutput.contains("--help-doc")) expect(helpOutput.contains("--help-repl")) expect(helpOutput.contains("--help-fmt")) } } for (fullHelpOptions <- HelpTests.fullHelpVariants) { lazy val fullHelp = os.proc(TestUtil.cli, fullHelpOptions).call(check = false) lazy val fullHelpOutput = fullHelp.out.trim() val fullHelpOptionsString = fullHelpOptions.mkString(" ") test(s"$fullHelpOptionsString works correctly") { assert( fullHelp.exitCode == 0, clues(fullHelpOptions, fullHelp.out.text(), fullHelp.err.text(), fullHelp.exitCode) ) expect(fullHelpOutput.contains("Usage:")) } test(s"$fullHelpOptionsString output includes legacy scala runner options") { expect(fullHelpOutput.contains("Legacy Scala runner options")) } } test("name aliases limited for standard help") { val help = os.proc(TestUtil.cli, "run", "--help").call() val helpOutput = help.out.trim() expect(TestUtil.removeAnsiColors(helpOutput).contains( "--jar, --extra-jars paths" )) } test("name aliases not limited for full help") { val help = os.proc(TestUtil.cli, "run", "--full-help").call() val helpOutput = help.out.trim() expect(TestUtil.removeAnsiColors(helpOutput).contains( "-cp, --jar, --jars, --class, --classes, -classpath, --extra-jar, --classpath, --extra-jars, --class-path, --extra-class, --extra-classes, --extra-class-path paths" )) } for { (subcommandLabel, leadArgs) <- Seq( ("compile subcommand", Seq("compile")), ("default subcommand", Seq.empty) ) } test(s"-opt-inline:help works without inputs ($subcommandLabel) (Scala 3.8.3+)") { TestInputs.empty.fromRoot { root => val res = os.proc(TestUtil.cli, leadArgs, "-S", Constants.scala3Next, "-opt-inline:help") .call(cwd = root, mergeErrIntoOut = true, check = false) expect(res.exitCode == 0) val out = res.out.text() expect(out.nonEmpty) expect(out.contains("Inlining requires")) } } for (withPower <- Seq(true, false)) test("envs help" + (if (withPower) " with power" else "")) { val powerOptions = if (withPower) Seq("--power") else Nil val help = os.proc(TestUtil.cli, "--envs-help", powerOptions).call() val helpOutput = help.out.trim() if (!withPower) expect(!helpOutput.contains("(power)")) expect(helpOutput.nonEmpty) expect(helpOutput.contains("environment variables")) } } object HelpTests { val variants = Seq( Seq("help"), Seq("help", "-help"), Seq("help", "--help"), Seq("-help"), Seq("--help") ) val fullHelpVariants = Seq( Seq("help", "--full-help"), Seq("help", "-full-help"), Seq("help", "--help-full"), Seq("help", "-help-full"), Seq("--full-help"), Seq("-full-help"), Seq("-help-full") ) } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/InstallAndUninstallCompletionsTests.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect import coursier.paths.shaded.dirs.ProjectDirectories import scala.util.Properties class InstallAndUninstallCompletionsTests extends ScalaCliSuite { val zshRcFile: String = ".zshrc" val bashRcFile: String = ".bashrc" val fishRcFile: String = "config.fish" val rcContent: String = s""" |dummy line |dummy line""".stripMargin val testInputs: TestInputs = TestInputs( os.rel / zshRcFile -> rcContent, os.rel / bashRcFile -> rcContent, os.rel / ".config" / "fish" / fishRcFile -> rcContent ) def runInstallAndUninstallCompletions(): Unit = { testInputs.fromRoot { root => val zshRcPath = root / zshRcFile val bashRcPath = root / bashRcFile val fishRcPath = root / ".config" / "fish" / fishRcFile // install completions to the dummy rc files os.proc(TestUtil.cli, "install-completions", "--rc-file", zshRcPath, "--shell", "zsh").call( cwd = root ) os.proc(TestUtil.cli, "install-completions", "--rc-file", bashRcPath, "--shell", "bash").call( cwd = root ) os.proc(TestUtil.cli, "install-completions", "--rc-file", fishRcPath, "--shell", "fish").call( cwd = root ) expect(os.read(bashRcPath).contains(bashRcScript)) expect(os.read(zshRcPath).contains(zshRcScript)) expect(os.read(fishRcPath).contains(fishRcScript)) // uninstall completions from the dummy rc files os.proc(TestUtil.cli, "uninstall-completions", "--rc-file", zshRcPath).call(cwd = root) os.proc(TestUtil.cli, "uninstall-completions", "--rc-file", bashRcPath).call(cwd = root) os.proc(TestUtil.cli, "uninstall-completions", "--rc-file", fishRcPath).call(cwd = root) expect(os.read(zshRcPath) == rcContent) expect(os.read(bashRcPath) == rcContent) expect(os.read(fishRcPath) == rcContent) } } def isWinShell: Boolean = Option(System.getenv("OSTYPE")).nonEmpty if (!Properties.isWin || isWinShell) test("installing and uninstalling completions") { runInstallAndUninstallCompletions() } lazy val bashRcScript: String = { val progName = "scala-cli" val ifs = "\\n" val script = s"""_${progName}_completions() { | local IFS=$$'$ifs' | eval "$$($progName complete bash-v1 "$$(( $$COMP_CWORD + 1 ))" "$${COMP_WORDS[@]}")" |} | |complete -F _${progName}_completions $progName |""".stripMargin addTags(script) } lazy val fishRcScript: String = { val progName = "scala-cli" val script = s"""complete $progName -a '($progName complete fish-v1 (math 1 + (count (__fish_print_cmd_args))) (__fish_print_cmd_args))'""" addTags(script) } lazy val zshRcScript: String = { val projDirs = ProjectDirectories.from(null, null, "ScalaCli") val dir = os.Path(projDirs.dataLocalDir, TestUtil.pwd) / "completions" / "zsh" val script = Seq( s"""fpath=("$dir" $$fpath)""", "compinit" ).map(_ + System.lineSeparator()).mkString addTags(script) } def addTags(script: String): String = { val start = "# >>> scala-cli completions >>>\n" val end = "# <<< scala-cli completions <<<\n" val withTags = "\n" + start + script.stripSuffix("\n") + "\n" + end withTags } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/InstallHomeTests.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect import scala.util.Properties class InstallHomeTests extends ScalaCliSuite { override def group: ScalaCliSuite.TestGroup = ScalaCliSuite.TestGroup.First val firstVersion = "0.0.1" val secondVersion = "0.0.2" val dummyScalaCliFirstName = "DummyScalaCli-1.scala" val dummyScalaCliSecondName = "DummyScalaCli-2.scala" val dummyScalaCliBinName = "scala-cli-dummy-test" val testInputs: TestInputs = TestInputs( os.rel / dummyScalaCliFirstName -> s""" |object DummyScalaCli extends App { | println(\"$firstVersion\") |}""".stripMargin, os.rel / dummyScalaCliSecondName -> s""" |object DummyScalaCli extends App { | println(\"$secondVersion\") |}""".stripMargin ) private def packageDummyScalaCli(root: os.Path, dummyScalaCliFileName: String, output: String) = { val cmd = Seq[os.Shellable]( TestUtil.cli, "--power", "package", dummyScalaCliFileName, "-o", output ) os.proc(cmd).call( cwd = root, stdin = os.Inherit, stdout = os.Inherit ) } private def installScalaCli( root: os.Path, binVersion: String, binDirPath: os.Path, force: Boolean ) = { val cmdInstallVersion = Seq[os.Shellable]( TestUtil.cli, "install-home", "--env", "--scala-cli-binary-path", binVersion, "--binary-name", dummyScalaCliBinName, "--bin-dir", binDirPath ) ++ (if (force) Seq[os.Shellable]("--force") else Seq.empty) os.proc(cmdInstallVersion).call( cwd = root, stdin = os.Inherit, stdout = os.Inherit ) } private def uninstallScalaCli( root: os.Path, binDirPath: os.Path, force: Boolean, skipCache: Boolean ) = { val cmdUninstall = Seq[os.Shellable]( TestUtil.cli, "uninstall", "--binary-name", dummyScalaCliBinName, "--bin-dir", binDirPath ) val forceOpts = if (force) Seq("--force") else Seq.empty val skipCacheOpts = if (skipCache) Seq("--skip-cache") else Seq.empty os.proc(cmdUninstall, forceOpts, skipCacheOpts).call(cwd = root) } def runInstallHome(): Unit = { testInputs.fromRoot { root => val binDirPath = root / Constants.workspaceDirName / "scala-cli" val binDummyScalaCliFirst = dummyScalaCliFirstName.stripSuffix(".scala").toLowerCase val binDummyScalaCliSecond = dummyScalaCliSecondName.stripSuffix(".scala").toLowerCase packageDummyScalaCli(root, dummyScalaCliFirstName, binDummyScalaCliFirst) packageDummyScalaCli(root, dummyScalaCliSecondName, binDummyScalaCliSecond) // install 1 version installScalaCli(root, binDummyScalaCliFirst, binDirPath, force = true) val v1Install = os.proc(binDirPath / dummyScalaCliBinName).call( cwd = root, stdin = os.Inherit ).out.trim() expect(v1Install == firstVersion) // update to 2 version installScalaCli(root, binDummyScalaCliSecond, binDirPath, force = false) val v2Update = os.proc(binDirPath / dummyScalaCliBinName).call( cwd = root, stdin = os.Inherit ).out.trim() expect(v2Update == secondVersion) // downgrade to 1 version with force installScalaCli(root, binDummyScalaCliFirst, binDirPath, force = true) val v1Downgrade = os.proc(binDirPath / dummyScalaCliBinName).call( cwd = root, stdin = os.Inherit ).out.trim() expect(v1Downgrade == firstVersion) uninstallScalaCli(root, binDirPath, force = true, skipCache = true) expect(!os.exists(binDirPath)) } } if (!Properties.isWin) test( "updating and downgrading dummy scala-cli using install-home command, uninstalling scala-cli using uninstall command" ) { runInstallHome() } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/JmhSuite.scala ================================================ package scala.cli.integration trait JmhSuite { this: ScalaCliSuite => protected def simpleBenchmarkingInputs(directivesString: String = ""): TestInputs = TestInputs( os.rel / "benchmark.scala" -> s"""$directivesString |package bench | |import java.util.concurrent.TimeUnit |import org.openjdk.jmh.annotations._ | |@BenchmarkMode(Array(Mode.AverageTime)) |@OutputTimeUnit(TimeUnit.NANOSECONDS) |@Warmup(iterations = 1, time = 100, timeUnit = TimeUnit.MILLISECONDS) |@Measurement(iterations = 10, time = 100, timeUnit = TimeUnit.MILLISECONDS) |@Fork(0) |class Benchmarks { | | @Benchmark | def foo(): Unit = { | (1L to 10000000L).sum | } | |} |""".stripMargin ) protected lazy val expectedInBenchmarkingOutput = """Result "bench.Benchmarks.foo":""" protected lazy val exampleOldJmhVersion = "1.29" } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/JmhTests.scala ================================================ package scala.cli.integration import ch.epfl.scala.bsp4j as b import com.eed3si9n.expecty.Expecty.expect import java.nio.file.Files import scala.cli.integration.TestUtil.await import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future import scala.jdk.CollectionConverters.* import scala.util.Properties class JmhTests extends ScalaCliSuite with JmhSuite with BspSuite { override def group: ScalaCliSuite.TestGroup = ScalaCliSuite.TestGroup.First override protected val extraOptions: Seq[String] = TestUtil.extraOptions for { useDirective <- Seq(None, Some("//> using jmh")) directiveString = useDirective.getOrElse("") jmhOptions = if (useDirective.isEmpty) Seq("--jmh") else Nil testMessage = useDirective match { case None => jmhOptions.mkString(" ") case Some(directive) => directive } } { test(s"run ($testMessage)") { // TODO extract running benchmarks to a separate scope, or a separate sub-command simpleBenchmarkingInputs(directiveString).fromRoot { root => val res = os.proc(TestUtil.cli, "--power", extraOptions, ".", jmhOptions).call(cwd = root) val output = res.out.trim() expect(output.contains(expectedInBenchmarkingOutput)) expect(output.contains(s"JMH version: ${Constants.jmhVersion}")) } } test(s"compile ($testMessage)") { simpleBenchmarkingInputs(directiveString).fromRoot { root => os.proc(TestUtil.cli, "compile", "--power", extraOptions, ".", jmhOptions) .call(cwd = root) } } test(s"doc ($testMessage)") { simpleBenchmarkingInputs(directiveString).fromRoot { root => val res = os.proc(TestUtil.cli, "doc", "--power", extraOptions, ".", jmhOptions) .call(cwd = root, stderr = os.Pipe) expect(!res.err.trim().contains("Error")) } } test(s"setup-ide ($testMessage)") { // TODO fix setting jmh via a reload & add tests for it simpleBenchmarkingInputs(directiveString).fromRoot { root => os.proc(TestUtil.cli, "setup-ide", "--power", extraOptions, ".", jmhOptions) .call(cwd = root) } } test(s"bsp ($testMessage)") { withBsp(simpleBenchmarkingInputs(directiveString), Seq(".", "--power") ++ jmhOptions) { (_, _, remoteServer) => Future { val buildTargetsResp = remoteServer.workspaceBuildTargets().asScala.await val targets = buildTargetsResp.getTargets.asScala.map(_.getId).toSeq expect(targets.length == 2) val compileResult = remoteServer.buildTargetCompile(new b.CompileParams(targets.asJava)).asScala.await expect(compileResult.getStatusCode == b.StatusCode.OK) } } } test(s"setup-ide + bsp ($testMessage)") { val inputs = simpleBenchmarkingInputs(directiveString) inputs.fromRoot { root => os.proc(TestUtil.cli, "setup-ide", "--power", extraOptions, ".", jmhOptions) .call(cwd = root) val ideOptionsPath = root / Constants.workspaceDirName / "ide-options-v2.json" expect(ideOptionsPath.toNIO.toFile.exists()) val ideLauncherOptsPath = root / Constants.workspaceDirName / "ide-launcher-options.json" expect(ideLauncherOptsPath.toNIO.toFile.exists()) val ideEnvsPath = root / Constants.workspaceDirName / "ide-envs.json" expect(ideEnvsPath.toNIO.toFile.exists()) val jsonOptions = List( "--json-options", ideOptionsPath.toString, "--json-launcher-options", ideLauncherOptsPath.toString, "--envs-file", ideEnvsPath.toString ) withBsp(inputs, Seq("."), bspOptions = jsonOptions, reuseRoot = Some(root)) { (_, _, remoteServer) => Future { val buildTargetsResp = remoteServer.workspaceBuildTargets().asScala.await val targets = buildTargetsResp.getTargets.asScala.map(_.getId).toSeq expect(targets.length == 2) val compileResult = remoteServer.buildTargetCompile(new b.CompileParams(targets.asJava)).asScala.await expect(compileResult.getStatusCode == b.StatusCode.OK) } } } } test(s"package ($testMessage)") { // TODO make package with --jmh build an artifact that actually runs benchmarks val expectedMessage = "Placeholder main method" simpleBenchmarkingInputs(directiveString) .add(os.rel / "Main.scala" -> s"""@main def main: Unit = println("$expectedMessage")""") .fromRoot { root => val launcherName = { val ext = if (Properties.isWin) ".bat" else "" "launcher" + ext } os.proc( TestUtil.cli, "package", "--power", TestUtil.extraOptions, ".", jmhOptions, "-o", launcherName ) .call(cwd = root) val launcher = root / launcherName expect(os.isFile(launcher)) expect(Files.isExecutable(launcher.toNIO)) val output = TestUtil.maybeUseBash(launcher)(cwd = root).out.trim() expect(output == expectedMessage) } } test(s"export ($testMessage)") { simpleBenchmarkingInputs(directiveString).fromRoot { root => // TODO add proper support for JMH export, we're checking if it doesn't fail the command for now os.proc(TestUtil.cli, "export", "--power", extraOptions, ".", jmhOptions) .call(cwd = root) } } } for { useDirective <- Seq(None, Some("//> using jmh false")) directiveString = useDirective.getOrElse("") jmhOptions = if (useDirective.isEmpty) Seq("--jmh=false") else Nil testMessage = useDirective match { case None => jmhOptions.mkString(" ") case Some(directives) => directives.linesIterator.mkString("; ") } if !Properties.isWin } test(s"should not compile when jmh is explicitly disabled ($testMessage)") { simpleBenchmarkingInputs(directiveString).fromRoot { root => val res = os.proc(TestUtil.cli, "compile", "--power", extraOptions, ".", jmhOptions) .call(cwd = root, check = false) expect(res.exitCode == 1) } } for { useDirective <- Seq( None, Some( s"""//> using jmh |//> using jmhVersion $exampleOldJmhVersion |""".stripMargin ) ) directiveString = useDirective.getOrElse("") jmhOptions = if (useDirective.isEmpty) Seq("--jmh", "--jmh-version", exampleOldJmhVersion) else Nil testMessage = useDirective match { case None => jmhOptions.mkString(" ") case Some(directives) => directives.linesIterator.mkString("; ") } if !Properties.isWin } test(s"should use the passed jmh version ($testMessage)") { simpleBenchmarkingInputs(directiveString).fromRoot { root => val res = os.proc(TestUtil.cli, "run", "--power", extraOptions, ".", jmhOptions) .call(cwd = root) val output = res.out.trim() expect(output.contains(expectedInBenchmarkingOutput)) expect(output.contains(s"JMH version: $exampleOldJmhVersion")) } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/LegacyScalaRunnerTestDefinitions.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect trait LegacyScalaRunnerTestDefinitions { this: DefaultTests => test("default to the run sub-command when a script snippet is passed with -e") { TestInputs.empty.fromRoot { root => val msg = "Hello world" val quotation = TestUtil.argQuotationMark val res = os.proc(TestUtil.cli, "-e", s"println($quotation$msg$quotation)", TestUtil.extraOptions) .call(cwd = root) expect(res.out.trim() == msg) } } test("running scala-cli with a script snippet passed with -e shouldn't allow repl-only options") { TestInputs.empty.fromRoot { root => val replSpecificOption = "--repl-dry-run" val res = os.proc( TestUtil.cli, "-e", "println()", replSpecificOption, TestUtil.extraOptions ) .call(cwd = root, mergeErrIntoOut = true, check = false) expect(res.exitCode == 1) expect(res.out.lines().endsWith(unrecognizedArgMessage(replSpecificOption))) } } test("ensure -save/--save works with the default command") { simpleLegacyOptionBackwardsCompatTest("-save", "--save") } test("ensure -nosave/--nosave works with the default command") { simpleLegacyOptionBackwardsCompatTest("-nosave", "--nosave") } test("ensure -howtorun/--how-to-run works with the default command") { legacyOptionBackwardsCompatTest("-howtorun", "--how-to-run") { (legacyHtrOption, inputFile, root) => Seq("object", "script", "jar", "repl", "guess", "invalid").foreach { htrValue => val res = os.proc(TestUtil.cli, legacyHtrOption, htrValue, inputFile, TestUtil.extraOptions) .call(cwd = root, stderr = os.Pipe) expect(res.err.trim().contains(deprecatedOptionWarning(legacyHtrOption))) expect(res.err.trim().contains(htrValue)) } } } test("ensure -I works with the default command") { legacyOptionBackwardsCompatTest("-I") { (legacyOption, inputFile, root) => val anotherInputFile = "smth.scala" val res = os.proc( TestUtil.cli, legacyOption, inputFile, legacyOption, anotherInputFile, "--repl-dry-run", TestUtil.extraOptions ) .call(cwd = root, stderr = os.Pipe) expect(res.err.trim().contains(deprecatedOptionWarning(legacyOption))) expect(res.err.trim().contains(inputFile)) expect(res.err.trim().contains(anotherInputFile)) } } test("ensure -nc/-nocompdaemon/--no-compilation-daemon works with the default command") { simpleLegacyOptionBackwardsCompatTest("-nc", "-nocompdaemon", "--no-compilation-daemon") } test("ensure -run works with the default command") { legacyOptionBackwardsCompatTest("-run") { (legacyOption, inputFile, root) => val res = os.proc(TestUtil.cli, legacyOption, inputFile, ".", TestUtil.extraOptions) .call(cwd = root, stderr = os.Pipe) expect(res.err.trim().contains(deprecatedOptionWarning(legacyOption))) expect(res.err.trim().contains(inputFile)) } } test("ensure -Yscriptrunner works with the default command") { legacyOptionBackwardsCompatTest("-Yscriptrunner") { (legacyOption, inputFile, root) => Seq("default", "resident", "shutdown", "scala.tools.nsc.fsc.ResidentScriptRunner").foreach { legacyOptionValue => val res = os.proc( TestUtil.cli, legacyOption, legacyOptionValue, inputFile, TestUtil.extraOptions ) .call(cwd = root, stderr = os.Pipe) expect(res.err.trim().contains(deprecatedOptionWarning(legacyOption))) expect(res.err.trim().contains(legacyOptionValue)) } } } private def simpleLegacyOptionBackwardsCompatTest(optionAliases: String*): Unit = abstractLegacyOptionBackwardsCompatTest(optionAliases) { (legacyOption, expectedMsg, _, root) => val res = os.proc(TestUtil.cli, legacyOption, "s.sc", TestUtil.extraOptions) .call(cwd = root, stderr = os.Pipe) expect(res.out.trim() == expectedMsg) expect(res.err.trim().contains(deprecatedOptionWarning(legacyOption))) } private def legacyOptionBackwardsCompatTest(optionAliases: String*)(f: ( String, String, os.Path ) => Unit): Unit = abstractLegacyOptionBackwardsCompatTest(optionAliases) { (legacyOption, _, inputFile, root) => f(legacyOption, inputFile, root) } private def abstractLegacyOptionBackwardsCompatTest(optionAliases: Seq[String])(f: ( String, String, String, os.Path ) => Unit): Unit = { val msg = "Hello world" val inputFile = "s.sc" TestInputs(os.rel / inputFile -> s"""println("$msg")""").fromRoot { root => optionAliases.foreach(f(_, msg, inputFile, root)) } } private def deprecatedOptionWarning(optionName: String) = s"Deprecated option '$optionName' is ignored" } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/LoggingTests.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect class LoggingTests extends ScalaCliSuite { test("single -q should not suppresses compilation errors") { TestInputs( os.rel / "Hello.scala" -> s"""object Hello extends App { | println("Hello" |} |""".stripMargin ).fromRoot { root => val res = os.proc(TestUtil.cli, ".", "-q").call(cwd = root, check = false, mergeErrIntoOut = true) val output = res.out.trim() expect(output.contains("Hello.scala:3:1")) expect(output.contains("error")) } } test("single -q should not suppresses output from app") { TestInputs( os.rel / "Hello.scala" -> s"""object Hello extends App { | println("Hello") |} |""".stripMargin ).fromRoot { root => val res = os.proc(TestUtil.cli, ".", "-q").call(cwd = root) val output = res.out.trim() expect(output.contains("Hello")) } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/MarkdownTests.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect class MarkdownTests extends ScalaCliSuite { test("run a simple .md file with a scala script snippet") { val expectedOutput = "Hello" TestInputs( os.rel / "sample.md" -> s"""# Sample Markdown file |A simple scala script snippet. |```scala |println("$expectedOutput") |``` |""".stripMargin ).fromRoot { root => val result = os.proc(TestUtil.cli, "sample.md").call(cwd = root) expect(result.out.trim() == expectedOutput) } } test("run a simple .md file with a scala raw snippet") { val expectedOutput = "Hello" TestInputs( os.rel / "sample.md" -> s"""# Sample Markdown file |A simple scala raw snippet. |```scala raw |object Hello extends App { | println("$expectedOutput") |} |``` |""".stripMargin ).fromRoot { root => val result = os.proc(TestUtil.cli, "sample.md").call(cwd = root) expect(result.out.trim() == expectedOutput) } } test("run a simple .md file with multiple interdependent scala snippets") { val expectedOutput = "Hello" TestInputs( os.rel / "sample.md" -> s"""# Sample Markdown file |## 1 |message case class |```scala |case class Message(value: String) |``` |## 2 |message declaration |```scala |val message = Message("$expectedOutput") |``` | |## 3 |output |```scala |println(message.value) |``` | |## |""".stripMargin ).fromRoot { root => val result = os.proc(TestUtil.cli, "sample.md").call(cwd = root) expect(result.out.trim() == expectedOutput) } } test("run a simple .md file with multiple scala snippets resetting the context") { val msg1 = "Hello" val msg2 = "world" val expectedOutput = s"$msg1 $msg2" TestInputs( os.rel / "sample.md" -> s"""# Sample Markdown file |## Scope 0 |```scala |val msg = "$msg1" |``` | |## Scope 1 |```scala reset |val msg = " " |``` | |## Scope 2 |```scala reset |val msg = "$msg2" |``` | |## Output |```scala reset |val msg = Scope.msg + Scope1.msg + Scope2.msg |println(msg) |``` |## |""".stripMargin ).fromRoot { root => val result = os.proc(TestUtil.cli, "sample.md").call(cwd = root) expect(result.out.trim() == expectedOutput) } } test("run markdown alongside other sources") { val msg1 = "Hello" val msg2 = " " val msg3 = "world" val msg4 = "!" val expectedOutput = msg1 + msg2 + msg3 + msg4 TestInputs( os.rel / "ScalaMessage.scala" -> "case class ScalaMessage(value: String)", os.rel / "JavaMessage.java" -> """public class JavaMessage { | public String value; | public JavaMessage(String value) { | this.value = value; | } |} |""".stripMargin, os.rel / "scripts" / "script.sc" -> "case class ScriptMessage(value: String)", os.rel / "Main.md" -> s"""# Main |Run it all from a snippet. |```scala |val javaMsg = new JavaMessage("$msg1") |val scalaMsg = ScalaMessage("$msg2") |val snippetMsg = snippet.SnippetMessage("$msg3") |val scriptMsg = scripts.script.ScriptMessage("$msg4") |val msg = javaMsg.value + scalaMsg.value + snippetMsg.value + scriptMsg.value |println(msg) |``` |""".stripMargin ).fromRoot { root => val snippetCode = "case class SnippetMessage(value: String)" val result = os.proc( TestUtil.cli, "--power", ".", "-e", snippetCode, "--markdown", "--main-class", "Main_md" ) .call(cwd = root) expect(result.out.trim() == expectedOutput) } } test("run a simple .md file with a using directive in a scala script snippet") { TestInputs( os.rel / "sample.md" -> s"""# Sample Markdown file |A simple scala script snippet. |```scala |//> using dep com.lihaoyi::os-lib:0.8.1 |println(os.pwd) |``` |""".stripMargin ).fromRoot { root => val result = os.proc(TestUtil.cli, "sample.md").call(cwd = root) expect(result.out.trim() == root.toString()) } } test("run a simple .md file with a using directive in a scala raw snippet") { TestInputs( os.rel / "sample.md" -> s"""# Sample Markdown file |A simple scala raw snippet. |```scala raw |//> using dep com.lihaoyi::os-lib:0.8.1 |object Hello extends App { | println(os.pwd) |} |``` |""".stripMargin ).fromRoot { root => val result = os.proc(TestUtil.cli, "sample.md").call(cwd = root) expect(result.out.trim() == root.toString()) } } test("run a simple .md file with multiple using directives spread across scala script snippets") { val msg1 = "Hello" val msg2 = "world" TestInputs( os.rel / "sample.md" -> s"""# Sample Markdown file |Let's try to spread the using directives into multiple simple `scala` code blocks of various kinds. | |## Circe |Let's depend on `circe-parser` in this one. |```scala |//> using dep io.circe::circe-parser:0.14.3 |import io.circe._, io.circe.parser._ |val json = \"\"\"{ "message": "$msg1"}\"\"\" |val parsed = parse(json).getOrElse(Json.Null) |val msg = parsed.hcursor.downField("message").as[String].getOrElse("") |println(msg) |``` | |## `pprint` |And `pprint`, too. |```scala |//> using dep com.lihaoyi::pprint:0.8.0 |pprint.PPrinter.BlackWhite.pprintln("$msg2") |``` | |## `os-lib` |And then on `os-lib`, just because. |And let's reset the scope for good measure, too. |```scala reset |//> using dep com.lihaoyi::os-lib:0.8.1 |val msg = os.pwd.toString |println(msg) |``` |""".stripMargin ).fromRoot { root => val result = os.proc(TestUtil.cli, "sample.md").call(cwd = root) val expectedOutput = s"""$msg1 |"$msg2" |$root""".stripMargin expect(result.out.trim() == expectedOutput) } } test("run a simple .md file with multiple using directives spread across scala raw snippets") { val msg1 = "Hello" val msg2 = "world" TestInputs( os.rel / "sample.md" -> s"""# Sample Markdown file |Let's try to spread the using directives into multiple simple `scala` code blocks of various kinds. | |## Circe |Let's depend on `circe-parser` in this one. |```scala raw |//> using dep io.circe::circe-parser:0.14.3 | |object CirceSnippet { | import io.circe._, io.circe.parser._ | | def printStuff(): Unit = { | val json = \"\"\"{ "message": "$msg1"}\"\"\" | val parsed = parse(json).getOrElse(Json.Null) | val msg = parsed.hcursor.downField("message").as[String].getOrElse("") | println(msg) | } |} |``` | |## `pprint` |And `pprint`, too. |```scala raw |//> using dep com.lihaoyi::pprint:0.8.0 |object PprintSnippet { | def printStuff(): Unit = | pprint.PPrinter.BlackWhite.pprintln("$msg2") |} |``` | |## `os-lib` |And then on `os-lib`, just because. |And let's reset the scope for good measure, too. |```scala raw |//> using dep com.lihaoyi::os-lib:0.8.1 | |object OsLibSnippet extends App { | CirceSnippet.printStuff() | PprintSnippet.printStuff() | println(os.pwd) |} |``` |""".stripMargin ).fromRoot { root => val result = os.proc(TestUtil.cli, "sample.md").call(cwd = root) val expectedOutput = s"""$msg1 |"$msg2" |$root""".stripMargin expect(result.out.trim() == expectedOutput) } } test("source file name start with a number") { val fileNameWithNumber = "01-intro.md" TestInputs( os.rel / fileNameWithNumber -> s"""# Introduction | |Welcome to the tutorial! | |```scala |println("Hello") |``` |""".stripMargin ).fromRoot { root => val result = os.proc(TestUtil.cli, fileNameWithNumber).call(cwd = root) expect(result.out.trim() == "Hello") } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/MavenTestHelper.scala ================================================ package scala.cli.integration trait MavenTestHelper { protected def mavenCommand(args: String*): os.proc = os.proc(maven, args) protected lazy val maven: os.Shellable = Seq[os.Shellable]( "mvn", "clean", "compile" ) } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/MetaCheck.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect class MetaCheck extends ScalaCliSuite { override def group: ScalaCliSuite.TestGroup = ScalaCliSuite.TestGroup.First /* * We don't run tests with --scala 3.… any more, and only rely on those * with no --scala option. * The test here ensures the default version is indeed Scala 3. */ test("Scala 3 is the default") { val testInputs = TestInputs( os.rel / "PrintScalaVersion.scala" -> """// https://gist.github.com/romanowski/de14691cab7340134e197419bc48919a | |object PrintScalaVersion extends App { | def props(url: java.net.URL): java.util.Properties = { | val properties = new java.util.Properties() | val is = url.openStream() | try { | properties.load(is) | properties | } finally is.close() | } | | def scala2Version: String = | props(getClass.getResource("/library.properties")).getProperty("version.number") | | def checkScala3(res: java.util.Enumeration[java.net.URL]): String = | if (!res.hasMoreElements) scala2Version else { | val manifest = props(res.nextElement) | manifest.getProperty("Specification-Title") match { | case "scala3-library-bootstrapped" => | manifest.getProperty("Implementation-Version") | case _ => checkScala3(res) | } | } | val manifests = getClass.getClassLoader.getResources("META-INF/MANIFEST.MF") | | val scalaVersion = checkScala3(manifests) | | println(scalaVersion) |} |""".stripMargin ) testInputs.fromRoot { root => // --ttl 0s so that we are sure we use the latest supported Scala versions listing val res = os.proc(TestUtil.cli, ".", "--ttl", "0s").call(cwd = root) val scalaVersion = res.out.trim() expect(scalaVersion == Constants.defaultScala) } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/MillTestHelper.scala ================================================ package scala.cli.integration import scala.util.Properties trait MillTestHelper { protected def millLauncher: os.RelPath = if (Properties.isWin) os.rel / "mill.bat" else os.rel / "mill" protected val millJvmOptsFileName: String = ".mill-jvm-opts" protected val millJvmOptsContent: String = """-Xmx512m |-Xms128m |""".stripMargin protected val millDefaultProjectName = "project" implicit class MillTestInputs(inputs: TestInputs) { def withMillJvmOpts: TestInputs = inputs.add(os.rel / millJvmOptsFileName -> millJvmOptsContent) } protected val millOutputDir: os.RelPath = os.rel / "output-project" protected def millCommand(root: os.Path, args: String*): os.proc = os.proc(root / millOutputDir / millLauncher, args) } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/NativePackagerTests.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect import scala.util.Properties class NativePackagerTests extends ScalaCliSuite { override def group: ScalaCliSuite.TestGroup = ScalaCliSuite.TestGroup.First override def munitFlakyOK: Boolean = TestUtil.isCI val helloWorldFileName = "HelloWorldScalaCli.scala" val message = "Hello, world!" val licencePath = "DummyLICENSE" val testInputs: TestInputs = TestInputs( os.rel / helloWorldFileName -> s""" |object HelloWorld { | def main(args: Array[String]): Unit = { | println("$message") | } |}""".stripMargin, os.rel / licencePath -> "LICENSE" ) private val ciOpt = Option(System.getenv("CI")).map(v => Seq("-e", s"CI=$v")).getOrElse(Nil) if (Properties.isMac) { test("building pkg package") { testInputs.fromRoot { root => val appName = helloWorldFileName.stripSuffix(".scala").toLowerCase val pkgAppFile = s"$appName.pkg" val cmd = Seq[os.Shellable]( TestUtil.cli, "--power", "package", TestUtil.extraOptions, helloWorldFileName, "--pkg", "--output", pkgAppFile, "--identifier", "scala-cli", "--launcher-app", appName ) os.proc(cmd).call( cwd = root, stdin = os.Inherit, stdout = os.Inherit ) val pkgAppPath = root / pkgAppFile expect(os.isFile(pkgAppPath)) if (TestUtil.isCI) { os.proc("installer", "-pkg", pkgAppFile, "-target", "CurrentUserHomeDirectory").call( cwd = root, stdin = os.Inherit, stdout = os.Inherit ) val home = sys.props("user.home") val output = os.proc(s"$home/Applications/$appName.app/Contents/MacOS/$appName") .call(cwd = os.root) .out.trim() expect(output == message) } } } def testBuildingDmgPackage(): Unit = testInputs.fromRoot { root => val appName = helloWorldFileName.stripSuffix(".scala").toLowerCase() val output = s"$appName.dmg" val cmd = Seq[os.Shellable]( TestUtil.cli, "--power", "package", TestUtil.extraOptions, helloWorldFileName, "--dmg", "--output", output, "--identifier", "scala-cli", "--launcher-app", appName ) os.proc(cmd).call( cwd = root, stdin = os.Inherit, stdout = os.Inherit ) val launcher = root / output expect(os.isFile(launcher)) if (TestUtil.isCI) { os.proc("hdiutil", "attach", launcher).call( cwd = root, stdin = os.Inherit, stdout = os.Inherit ) val output = os.proc(s"/Volumes/$appName/$appName.app/Contents/MacOS/$appName") .call(cwd = os.root) .out.trim() expect(output == message) os.proc("hdiutil", "detach", s"/Volumes/$appName").call( cwd = root, stdin = os.Inherit, stdout = os.Inherit ) } } // FIXME: building dmg package sometimes fails with: // 'hdiutil: couldn't eject "disk2" - Resource busy' if (TestUtil.isAarch64 || !TestUtil.isCI) test("building dmg package") { testBuildingDmgPackage() } else test("building dmg package".flaky) { testBuildingDmgPackage() } } if (Properties.isLinux) { test("building deb package") { testInputs.fromRoot { root => val appName = helloWorldFileName.stripSuffix(".scala").toLowerCase() val priority = "optional" val section = "devel" val destDir = os.rel / "package" os.makeDir.all(root / destDir) val cmd = Seq[os.Shellable]( TestUtil.cli, "--power", "package", TestUtil.extraOptions, helloWorldFileName, "--deb", "--output", destDir / s"$appName.deb", "--maintainer", "scala-cli-test", "--description", "scala-cli-test", "--launcher-app", appName, "--priority", priority, "--section", section ) os.proc(cmd).call( cwd = root, stdin = os.Inherit, stdout = os.Inherit ) val launcher = root / destDir / s"$appName.deb" expect(os.isFile(launcher)) // check flags val debInfo = os.proc("dpkg", "--info", launcher).call().out.text().trim expect(debInfo.contains(s"Priority: $priority")) expect(debInfo.contains(s"Section: $section")) if (hasDocker) { val script = s"""#!/usr/bin/env bash |set -e |dpkg -x "$appName.deb" . |exec ./usr/share/scala/$appName |""".stripMargin os.write(root / destDir / "run.sh", script) os.perms.set(root / destDir / "run.sh", "rwxr-xr-x") val termOpt = if (System.console() == null) Nil else Seq("-t") val ciOpt = Option(System.getenv("CI")).map(v => Seq("-e", s"CI=$v")).getOrElse(Nil) val res = os.proc( "docker", "run", termOpt, ciOpt, "--rm", "-w", "/workdir", "-v", s"${root / destDir}:/workdir", "eclipse-temurin:17-jdk", "./run.sh" ).call( cwd = root, stdout = os.Pipe, mergeErrIntoOut = true ) expect(res.exitCode == 0) val output = res.out.trim() expect(output.endsWith(message)) } } } test("building rpm package") { testInputs.fromRoot { root => val appName = helloWorldFileName.stripSuffix(".scala").toLowerCase() val cmd = Seq[os.Shellable]( TestUtil.cli, "--power", "package", TestUtil.extraOptions, helloWorldFileName, "--rpm", "--output", s"$appName.rpm", "--description", "scala-cli", "--license", "ASL 2.0", "--version", "1.0.0", "--launcher-app", appName ) os.proc(cmd).call( cwd = root, stdin = os.Inherit, stdout = os.Inherit ) val launcher = root / s"$appName.rpm" expect(os.isFile(launcher)) } } } if (Properties.isWin) test("building msi package") { testInputs.fromRoot { root => val appName = helloWorldFileName.stripSuffix(".scala").toLowerCase() val cmd = Seq[os.Shellable]( TestUtil.cli, "--power", "package", helloWorldFileName, "--msi", "--output", s"$appName.msi", "--product-name", "scala-cli", "--license-path", licencePath, "--maintainer", "Scala-CLI", "--launcher-app", appName, "--suppress-validation" ) os.proc(cmd).call( cwd = root, stdin = os.Inherit, stdout = os.Inherit ) val launcher = root / s"$appName.msi" expect(os.isFile(launcher)) } } def runTest(): Unit = testInputs.fromRoot { root => val appName = helloWorldFileName.stripSuffix(".scala") val imageRepository = appName.toLowerCase() val imageTag = "latest" val cmd = Seq[os.Shellable]( TestUtil.cli, "--power", "package", helloWorldFileName, "--docker", "--docker-image-repository", imageRepository, "--docker-image-tag", imageTag ) os.proc(cmd) .call( cwd = root, stdin = os.Inherit, stdout = os.Inherit ) val expectedImage = s"$imageRepository:$imageTag" try { val output = os.proc("docker", "run", ciOpt, expectedImage).call(cwd = os.root).out.trim() expect(output == message) } // clear finally os.proc("docker", "rmi", "-f", expectedImage).call(cwd = os.root) } def runJsTest(): Unit = testInputs.fromRoot { root => val appName = helloWorldFileName.stripSuffix(".scala") val imageRepository = appName.toLowerCase() val imageTag = "latest" val cmd = Seq[os.Shellable]( TestUtil.cli, "--power", "package", helloWorldFileName, "--js", "--docker", "--docker-image-repository", imageRepository, "--docker-image-tag", imageTag ) // format: on os.proc(cmd) .call( cwd = root, stdin = os.Inherit, stdout = os.Inherit ) val expectedImage = s"$imageRepository:$imageTag" try { val output = os.proc("docker", "run", ciOpt, expectedImage).call(cwd = os.root).out.trim() expect(output == message) } // clear finally os.proc("docker", "rmi", "-f", expectedImage).call(cwd = os.root) } def runNativeTest(): Unit = testInputs.fromRoot { root => val appName = helloWorldFileName.stripSuffix(".scala") val imageRepository = appName.toLowerCase() val imageTag = "latest" val cmd = Seq[os.Shellable]( TestUtil.cli, "--power", "package", helloWorldFileName, "--native", "-S", "2.13", "--docker", "--docker-image-repository", imageRepository, "--docker-image-tag", imageTag ) os.proc(cmd) .call( cwd = root, stdin = os.Inherit, stdout = os.Inherit ) val expectedImage = s"$imageRepository:$imageTag" try { val output = os.proc("docker", "run", ciOpt, expectedImage).call(cwd = os.root).out.trim() expect(output == message) } // clear finally os.proc("docker", "rmi", "-f", expectedImage).call(cwd = os.root) } def hasDocker: Boolean = Properties.isLinux || // no docker command or no Linux from it on Github actions macOS / Windows runners ((Properties.isMac || Properties.isWin) && !TestUtil.isCI) if (hasDocker) { // TODO: restore this test when `registry-1.docker.io` is stable again test("building docker image".flaky) { TestUtil.retryOnCi() { runTest() } } // FIXME for some reason, this test became flaky on the CI if (TestUtil.isNativeCli) test("building docker image with scala.js app".flaky) { TestUtil.retryOnCi() { runJsTest() } } else test("building docker image with scala.js app") { TestUtil.retryOnCi() { runJsTest() } } } if (Properties.isLinux) // FIXME this got flaky on the CI again test("building docker image with scala native app".flaky) { TestUtil.retryOnCi() { runNativeTest() } } } ================================================ FILE: modules/integration/src/test/scala/scala/cli/integration/NewTests.scala ================================================ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect import scala.cli.integration.TestUtil.removeAnsiColors class NewTests extends ScalaCliSuite { override def group: ScalaCliSuite.TestGroup = ScalaCliSuite.TestGroup.First val simpleTemplateName = "VirtusLab/scala-cli.g8" val expectedTemplateContent = """ |object HelloWorld { | def main(args: Array[String]): Unit = { | println("Hello, world!") | } |}""".stripMargin val simpleTemplate: TestInputs = TestInputs( os.rel / "HelloWorld.scala" -> expectedTemplateContent ) test("simple new template") { simpleTemplate.fromRoot { root => os.proc(TestUtil.cli, "--power", "new", simpleTemplateName).call(cwd = root) val content = os.read(root / "HelloWorld.scala") expect(content == expectedTemplateContent) } } test("error missing template argument") { TestInputs.empty.fromRoot { root => val result = os.proc(TestUtil.cli, "--power", "new").call( cwd = root, check = false, mergeErrIntoOut = true ) val lines = removeAnsiColors(result.out.text()).linesIterator.toVector assert(result.exitCode == 1) expect(lines.contains("Error: Missing argument