Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade validation compare deps #19184

Open
wants to merge 12 commits into
base: main
Choose a base branch
from

Conversation

dylant-da
Copy link
Contributor

@dylant-da dylant-da commented May 13, 2024

If package2 upgrades package1, all of package2's dependencies should also upgrade their corresponding package1's dependencies

So if mylib-1.0.0 depends on mydep-1.0.0, then mylib-2.0.0 must depend on a mydep >= 1.0.0. If mydep has a version strictly less than 1.0.0, then the dependency downgrades while the package itself upgrades, in which case we should error out.

Will be rebased on top of #19123, we should complete a review of that first done

@dylant-da dylant-da force-pushed the upgrade-validation-compare-deps branch from 61ba239 to a167cc3 Compare May 13, 2024 09:32
@dylant-da dylant-da marked this pull request as ready for review May 13, 2024 15:26
@dylant-da dylant-da marked this pull request as draft May 13, 2024 15:26
@dylant-da dylant-da marked this pull request as ready for review May 21, 2024 13:26
@dylant-da dylant-da force-pushed the upgrade-validation-compare-deps branch from 0e5a3d8 to 5a6d7e0 Compare May 23, 2024 14:02
@dylant-da
Copy link
Contributor Author

I've split this PR into its three pieces (daml, canton, daml) to check that they all work - some additional work is necessary for the first change to pass, we'll remove it in the last step.

Copy link
Contributor Author

@dylant-da dylant-da left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Annotated the code in a few spots to help review, as well as ask a few questions

oldPkgId2: Ref.PackageId,
optOldPkg2: Option[Ast.Package],
optOldPkg2: Option[(Ast.Package, PackageMap)],
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We pass in a packagemap of dependencies with each package - this map only contains packageids of deps and their corresponding names and versions, so that we can check that each package's version increases correctly.

@@ -199,9 +204,9 @@ class PackageUpgradeValidator(
case (Some((newPkgId1, newPkg1)), Some((oldPkgId2, oldPkg2))) =>
strictTypecheckUpgrades(
typecheckPhase,
Some((newPkgId1, newPkg1)),
Some((newPkgId1, newPkg1, newPkg1.directDeps.map((x: Ref.PackageId) => (x, packageMap(x))).toMap)),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we generate the dependency map for a package from directDeps which is populated during the decoding phase.

An important caveat is that if a dependency does not get used (none of its identifiers show up in the depending package), the dep will show up in the manifest file but will not show up in the directDeps list.

However, I don't think this is much of an issue, it only makes compiler-side checks more restrictive than participant, and the workaround to make it pass on the compiler-side is not to list an unused dependency, which is good code hygiene.

If anything, we should be removing dependencies from the manifest that aren't actually used - should I create a tickt for this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed on zoom, I think this will let someone sneakily upload v2 of a package (via a dep), without canton checking that v3 upgrades v2 and v2 upgrades v1.

Also as discussed, this is probably not the place to check for that and should be part of another PR.


-- | Take a package version of regex "(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*" into
-- a list of integers [Integer]
splitPackageVersion :: PackageVersion -> Maybe [Integer]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

utils for versions and comparing them

| SecondVersionUnparseable PackageVersion
deriving (Show, Eq, Ord)

comparePackageVersion :: PackageVersion -> PackageVersion -> Either ComparePackageVersionError Ordering
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we don't use the 0.0.0 convention in other code because (I assume) packages don't adhere to that convention - is that true?

@@ -694,6 +702,10 @@ instance Pretty Warning where
[ "The template " <> pPrint tpl <> " has implemented interface " <> pPrint iface <> ", which is defined in a previous version of this package."
, "However, it is recommended that interfaces are defined in their own package separate from their implementations."
]
WPastDependencyHasUnparseableVersion pkgName version ->
"Dependency " <> pPrint pkgName <> " of upgrading package has a version which cannot be parsed: '" <> pPrint version <> "'"
WPresentDependencyHasUnparseableVersion pkgName version ->
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Backport to 2.x will need to deal with metadata being potentially missing from a dependency.

@@ -353,18 +358,18 @@ tests damlc =
)
let sharedDir = dir </> "shared"
let sharedDar = sharedDir </> "out.dar"
writeFiles sharedDir (projectFile lfVersion ("upgrades-example-" <> name <> "-dep") Nothing Nothing : sharedDepFiles)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ugly mess of test code generates daml.yamls and appropriate directories from groups of test files in test-common/.... It's messy but ultimately is just handling the different ways we might want to build a series of packages for upgrading.

tc.check()
}
}

def typecheckUpgrades(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We add this function with the previous interface that didn't take dependencies in order to be able to merge the first part of this PR and build canton against it in step 1 of the merge dance. Part 1: #19274

We remove this function in part 3: #19275

_ <- tryAll(upgradedModules.values, checkModule(_))
} yield ()
}

private def checkDeps(): Try[Unit] = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate the dependency logic from the compiler inside the participant.

private def checkDep(dep: (Ref.PackageName, Upgrading[Ref.PackageVersion])): Try[Unit] = {
val (depName, depVersions) = dep
failIf(
depVersions.present < depVersions.past,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There already is a "comparable" instance for Ref.PackageVersion that does the correct thing, so we don't need any utils like we did with Haskell

import Dep

self : Text
self = "v1 with " <> dep
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have to actually use the dependency, otherwise it won't show up during participant validation in directDeps

Copy link
Contributor

@paulbrauner-da paulbrauner-da left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@@ -199,9 +204,9 @@ class PackageUpgradeValidator(
case (Some((newPkgId1, newPkg1)), Some((oldPkgId2, oldPkg2))) =>
strictTypecheckUpgrades(
typecheckPhase,
Some((newPkgId1, newPkg1)),
Some((newPkgId1, newPkg1, newPkg1.directDeps.map((x: Ref.PackageId) => (x, packageMap(x))).toMap)),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed on zoom, I think this will let someone sneakily upload v2 of a package (via a dep), without canton checking that v3 upgrades v2 and v2 upgrades v1.

Also as discussed, this is probably not the place to check for that and should be part of another PR.

val internedTypes = Work.run(decodeInternedTypes(env0, lfPackage))
val env = env0.copy(internedTypes = internedTypes)

val modules = lfPackage.getModulesList.asScala.map(env.decodeModule(_))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that a leftover from a refactoring that didn't happen, or some debugging maybe? It's fine as it is, but I want to make sure you weren't planning on making a change here and then forgot about it.

Copy link
Contributor Author

@dylant-da dylant-da May 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided to move the line here, since it's side-effectful in a way that affects the return value of directDeps in Package.build, to prevent any problems that would occur if the lines were re-ordered.

Comment on lines 349 to 351
let pad xs target
| length xs < length target = xs ++ replicate (length target - length xs) 0
| otherwise = xs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can make that a bit shorter and (in my opinion) more readable:

let pad xs target = take (length target) (xs ++ repeat 0)

Copy link
Contributor Author

@dylant-da dylant-da May 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct me if I'm wrong, if the target is shorter than xs, then won't this drop elements from xs? We want to keep the maximum of the two.

That said, a hybrid solution would work best:

let pad xs target = take (length target `max` length xs) (xs ++ repeat 0)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, sorry about that!

- Change _package to literal identifier `package`
- Use type synonym PackageMap where possible
- Fix binding of _new and _deleted in wrong order
- Refactor `splitPackageVersion` and `comparePackageVersion`
@@ -343,15 +343,15 @@ case class TypecheckUpgrades(
) {
import TypecheckUpgrades._

private lazy val _package: Upgrading[Ast.Package] = packages.map(_._2)
private lazy val `package`: Upgrading[Ast.Package] = packages.map(_._2)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh of course, I understand why the underscore now 🤦

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants